/* =============================================================================
   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 {
	/* The size of ordinary reading text, and of the navigation. Components that
	   are plain text at the page's own size take this rather than a pixel value,
	   so the whole interface can be scaled in one place. */
	/* The type scale, in rem so it follows the browser's own text size. 12px is the
	   floor — nothing is smaller — and the steps above the base are about 1.125x
	   apart. A component takes a token rather than a pixel value, so the whole
	   interface can be rescaled here. */
	--ip-font-xs:       0.75rem;    /* footnotes, meta, tags */
	--ip-font-sm:       0.875rem;   /* subtitles, compact controls */
	--ip-font-base:     1rem;       /* body text, buttons, navigation */
	--ip-font-h3:       1.125rem;   /* card and section titles */
	--ip-font-h2:       1.25rem;    /* modal, flyout and settings titles */
	--ip-font-h1:       1.5rem;     /* page and sign-in titles */
	/* The pit display's own scale, and the one exception to the six steps above.
	   A display is read from across a pit aisle, on a 1920×1080 stage that is
	   then scaled to the screen (see "Pit display"), so its sizes answer to that
	   distance rather than to a desk. Used by .ip-show-* and nothing else. */
	--ip-show-font-meta:   1.75rem;  /* captions, the footer, labels under figures */
	--ip-show-font-body:   2.25rem;  /* a paragraph on a slide */
	--ip-show-font-head:   3.5rem;   /* a slide's heading */
	--ip-show-font-title:  5.5rem;   /* the organisation's name, a spotlight's name */
	--ip-show-font-figure: 9rem;     /* a headline figure */
	/* Icons beside text, all in em so they follow the text's own size: a nudge or a
	   stroke in pixels is right for one size and wrong for every other. */
	--ip-icon-nudge:    0.07em;     /* lowercase sits below its line's middle */
	--ip-icon-scale:    0.875em;    /* a glyph fills its whole box; text does not */
	--ip-icon-stroke:   0.05em;     /* thickens the plus to match 600-weight text */
	/* One height for everything in the top bar: the award window, the team switcher
	   and the icon buttons. It is what .ip-seg comes to by itself (3px padding, a
	   6px-padded 14px label, borders), so the bar matches the view switcher on a page;
	   a pointer that is coarse raises it to a touch target below. */
	--ip-control-h:     41px;
	/* Where anything that sticks while the page scrolls comes to rest: a page's
	   padding below the top bar, so it keeps the air it had before scrolling.
	   One value for table headers, side panes and the settings menu, or two
	   pinned things side by side stop at different heights. --ip-topbar-h is
	   the bar's measured height, set by table-layout.js. */
	--ip-stick-top:     calc(var(--ip-topbar-h, 62px) + 16px);
	--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.72);
	--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-green:         #15803d;
	--ip-green-bg:      #e8f7ee;
	--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;

	/* The modules of a QR code, drawn on --ip-logo-ground. Fixed for the same
	   kind of reason: a code is read by a phone's camera, and not every camera
	   reads one inverted, so it is dark on light in both themes. */
	--ip-code-ink:      #111111;

	/* 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.
	   All four take dark text: white on the FRC blue and the FTC orange came to
	   about 3:1 and 2.7:1 at tag size, short of the 4.5:1 small text needs.
	   Solid, they are for a mark read from afar (the pit display) and a chosen
	   program in the picker. */
	--ip-frc:           #009cd7;
	--ip-frc-fg:        #222222;
	--ip-ftc:           #f57e25;
	--ip-ftc-fg:        #222222;
	--ip-fll:           #ffc107;
	--ip-fll-fg:        #222222;
	--ip-fgc:           #00a651;
	--ip-fgc-fg:        #222222;
	/* The same hues as a tag, the way .ip-tag draws a type: a tint and the
	   colour as text. These do flip with the theme — FIRST's yellow as text on
	   a pale ground is unreadable — so each is the program's hue taken dark
	   enough here, light enough in the dark theme, for 4.5:1 on its tint. */
	--ip-frc-bg:        #e3f4fb;
	--ip-frc-ink:       #00668f;
	--ip-ftc-bg:        #fdeee2;
	--ip-ftc-ink:       #a3470b;
	--ip-fll-bg:        #fff4d1;
	--ip-fll-ink:       #805a00;
	--ip-fgc-bg:        #e2f5ea;
	--ip-fgc-ink:       #00703a;
	--ip-input-bg:      #ffffff;
	/* The map's ground: land, water and the lines on them, kept quiet so the
	   places on it are what stands out. --ip-map-space is behind the globe;
	   --ip-map-star is the dark theme's alone. */
	--ip-map-space:     #e6ecea;
	--ip-map-land:      #fbfcfc;
	--ip-map-water:     #d3e2df;
	--ip-map-park:      #edf3ee;
	--ip-map-border:    #b3bfbb;
	--ip-map-road:      #e4e9e7;
	--ip-map-building:  #e1e7e5;
	--ip-map-star:      transparent;
	/* How much of the air round the globe shows, 0–1. Less in light: the
	   halo is white, and at full strength it fogged the land under it. */
	--ip-map-air:       0.45;
	--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);

	/* The keyboard focus ring. Resolved on :root against whichever teal the
	   theme sets there, so one declaration covers both. */
	--ip-focus:         var(--ip-teal);

	--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);


	/* 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.72);
	--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-green:         #86d9a3;
	--ip-green-bg:      #12301f;
	--ip-violet:        #c0aefc;
	--ip-violet-bg:     #241c40;
	--ip-pink:          #f0a2c6;
	--ip-pink-bg:       #37162a;
	--ip-frc-bg:        #0f2a36;
	--ip-frc-ink:       #6cccf2;
	--ip-ftc-bg:        #36220f;
	--ip-ftc-ink:       #f9a76b;
	--ip-fll-bg:        #332a0b;
	--ip-fll-ink:       #ffd356;
	--ip-fgc-bg:        #0e2e1c;
	--ip-fgc-ink:       #5cd394;

	--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-map-space:     #030405;
	--ip-map-land:      #20272a;
	--ip-map-water:     #0a0e0f;
	--ip-map-park:      #1c2723;
	--ip-map-border:    #4a5a5c;
	--ip-map-road:      #2b3436;
	--ip-map-building:  #262e31;
	--ip-map-star:      rgba(255, 255, 255, 0.75);
	--ip-map-air:       1;
	--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);

	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);
	font-size: var(--ip-font-base);
	-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; }
/* Exactly the visible height, not 100vh. On a phone vh is the viewport with the
   browser's toolbars hidden — and they never hide here, because the page itself
   never scrolls (#app-main does). So a 100vh shell ran a toolbar's height past
   the bottom of the screen, where the last of every page sat out of reach. */
body.app-shell { height: 100vh; height: 100dvh; }

/* For the one frame a theme switch takes (HeadScripts.php): the new palette
   lands at once instead of starting an ease on every control in the page. */
:root.ip-theme-switching,
:root.ip-theme-switching *,
:root.ip-theme-switching *::before,
:root.ip-theme-switching *::after { transition: none !important; }

/* 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; }

/* One focus ring for everything reached by Tab. Left to the browser, a button,
   a pill and a sidebar link each drew a different one, and some drew a ring the
   dark theme swallowed. `:where` keeps it at zero specificity, so a control
   that needs another shape — a tile over a photo — still overrides it. Text
   inputs keep their own mint ring on :focus, which shows on a click too. */
:where(a, button, summary, [tabindex], [role="button"]):focus-visible {
	outline: 2px solid var(--ip-focus);
	outline-offset: 2px;
}
/* Bootstrap's own controls draw a shadow ring and switch the outline off with
   more specificity than the rule above, so they are given the same ring here. */
.nav-link:focus-visible,
.dropdown-item:focus-visible,
.btn:focus-visible,
.btn-close:focus-visible {
	box-shadow: none;
	outline: 2px solid var(--ip-focus);
	outline-offset: 2px;
}
.dropdown-item:focus-visible { outline-offset: -2px; }

/* 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: var(--ip-font-base);
	/* 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 picker glyphs are drawn by the browser and follow `color-scheme`, which
   the dark block sets — so they are left alone. An invert used to sit here as
   well, and in dark it undid the browser's own light glyph, leaving the date
   field's calendar dark on dark while the time field beside it was fine. */
input::-webkit-calendar-picker-indicator { cursor: pointer; }
::placeholder { color: var(--ip-placeholder); opacity: 1; }

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

/* A page arrives by fading in, not by sliding: a page that moves up into place
   as it appears reads as the content shifting under the reader's eye, and on
   the way out it was the same motion in reverse. */
.ip-rise { animation: app-page-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; }
	/* Things that travel into place arrive instead. Opacity changes stay where
	   they carry meaning; a slide or a rise never does. */
	.ip-rise,
	.ip-modal__panel,
	.ip-flyout,
	.ip-flyout__scrim { animation: none; }
	html { scroll-behavior: auto; }
}


.ip-mono { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }

/* -----------------------------------------------------------------------------
   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.

   Positioned so it is the containing block for anything absolute inside a
   page. Without that, an absolute element with no positioned ancestor (a
   skeleton's .visually-hidden "Loading") sat outside the column, made the
   document taller than the window, and gave scrollIntoView() somewhere to
   go: a jump to a short last section — Export data — scrolled the window
   itself and lifted the whole shell off the top of the screen. */
#app-main { position: relative; 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
	     1070  .ip-tour, which points at this bar and so must cover all of it
	   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);
	/* Thin enough to see the page moving under it, blurred and saturated enough
	   that what shows through is colour and movement rather than legible text
	   competing with the title. At 90% opaque the old 8px blur was invisible —
	   the cost of a backdrop filter for none of the effect. */
	backdrop-filter: blur(20px) saturate(180%);
	-webkit-backdrop-filter: blur(20px) saturate(180%);
	/* Only while something is scrolled under it (impact-shell.js). At the top
	   of a page there is nothing to divide the bar from. */
	border-bottom: 1px solid transparent;
	transition: border-color 160ms ease;
	flex-shrink: 0;
}
.app-topbar.is-scrolled { border-bottom-color: var(--ip-border-soft); }
/* Asked for less see-through: the bar becomes the solid ground it sits on. */
@media (prefers-reduced-transparency: reduce) {
	.app-topbar { background: var(--ip-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
@media (prefers-contrast: more) {
	.app-topbar { background: var(--ip-bg); backdrop-filter: none; -webkit-backdrop-filter: none; border-bottom-color: var(--ip-border); }
}
.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: var(--ip-font-h1);
}
/* The heading is as tall as a title plus a subtitle whether or not there is a
   subtitle, so the bar never changes height between pages — and the content is
   centred in it, so a title with no subtitle sits mid-bar rather than at the top
   of a gap left for words that are not coming. */
.app-topbar__heading {
	flex: 1 1 200px;
	min-width: 0;
	/* A title and a subtitle, not a title alone: a page without its subtitle yet
	   (one being navigated to) or without one at all would otherwise be a bar
	   shorter by the subtitle's line, and everything below it moved that far
	   as the words arrived. */
	min-height: 52px;
	display: flex;
	flex-direction: column;
	justify-content: center;
}
.app-topbar__title {
	margin: 0;
	font-size: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.015em;
	line-height: 1.3;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* An empty one is dropped (below) and the heading's own min-height holds the
   bar's height instead. Hiding it without that made the bar a few pixels
   shorter on those pages, so moving between a page with a subtitle and one
   without nudged everything below the bar up and down. */
.app-topbar__sub {
	margin-top: 1px;
	min-height: 1.4em;
	font-size: var(--ip-font-sm);
	line-height: 1.4;
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.app-topbar__sub:empty { display: none; }
/* 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;
	/* Top, in a row one pixel taller than the 41px controls: centred, an odd
	   height in the bar's even one (42 bare, 52 with a subtitle) sat on a half
	   pixel. At rest the browser snaps a button's box to the grid and its glyph
	   separately, so the icon sat a pixel off its designed place, and a press —
	   a scale, which nothing snaps — dropped it back there: the icon jumped. An
	   even row centres on a whole pixel; inert where a touch screen makes the
	   controls 44px. */
	align-items: flex-start;
	min-height: 42px;
	flex-wrap: wrap;
	gap: 8px;
	margin-left: auto;
}
/* The one-team label is text, not a control, and shorter than the row: it is
   never pressed, so it centres as everything did before. */
#ip-team-current { align-self: center; }
/* 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: var(--ip-control-h); align-items: stretch; }
.app-topbar__tools .ip-seg__btn { padding: 0 11px; }
/* The wrapper Settings → Teams redraws into. It has no box of its own, so the
   switcher's items stay direct children of the tools row and its gap and
   wrapping are unchanged. */
#ip-team-switch { display: contents; }
.app-topbar__tools .app-topbar__team {
	height: var(--ip-control-h);
	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);
}
/* The award window's labels — the long one, and a short one for a phone. */
.ip-seg__short { display: none; }

/* A phone. The bar was three rows — title, award window, team and theme — and
   at 157px a fifth of the screen, kept there while everything else scrolled.
   The tools now share one row under the title. */
@media (max-width: 575.98px) {
	.app-topbar { padding: 8px 12px; gap: 6px 10px; }
	.app-topbar__tools { flex-wrap: nowrap; width: 100%; margin-left: 0; }
	.app-topbar__tools > :last-child { margin-left: auto; }
	.ip-seg__long { display: none; }
	.ip-seg__short { display: inline; }
	/* At 44px a control (the touch size, below) the row came to about 400px
	   and ran off the right of most phones, taking the theme toggle and half
	   the bell with it. Tighter options and gaps bring it inside 360px, the
	   narrowest common Android width. */
	.app-topbar__tools { gap: 5px; }
	.app-topbar__tools .ip-seg__btn { padding: 0 7px; }
	.app-topbar__tools .app-topbar__team { padding: 0 8px 0 6px; gap: 6px; }
	.app-topbar__tools .app-topbar__team.dropdown-toggle::after { margin-left: 0; }
}
/* Narrower still, one row cannot hold them: the award window takes a row of
   its own and the team, bell and theme share the next — taller, but every
   control is on the screen. */
@media (max-width: 359.98px) {
	.app-topbar__tools { flex-wrap: wrap; }
	.app-topbar__tools .ip-seg { flex: 1 0 100%; }
	.app-topbar__tools .ip-seg__btn { flex: 1 1 0; justify-content: center; }
}

.app-topbar__team-name {
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	min-width: 0;
}
/* Hidden below the laptop breakpoint (next rule), which is why this is not a
   .d-none .d-md-inline pair: those are !important and held the name on
   screen from 768px, past the point where it pushed the title into an ellipsis. */
@media (max-width: 575.98px) {
	.app-topbar__team-name { display: none; }
}
/* A laptop. Beside the sidebar the bar has ~740px, and the award window, a team
   name, the bell and the theme toggle together are wider than what the title
   leaves: the whole row dropped under the title, and the title sat in a bar
   with its right half empty. The team's number is what tells teams apart, so
   the name gives way first — the menu it opens still lists them in full. */
@media (min-width: 576px) and (max-width: 1200px) {
	.app-topbar__team-name { display: none; }
	/* The window's short labels too: the long ones are what pushed the tools under the title. */
	.ip-seg__long { display: none; }
	.ip-seg__short { display: inline; }
}

#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);
}
/* The portal's mark, where an organisation has no logo of its own. It comes in
   two cuts — a deep track for the dark theme, a pale one for the light — and
   both are in the markup, because the theme is an attribute somebody chose,
   not a media query a <picture> could follow. */
:root[data-theme="light"] .ip-mark--dark,
:root:not([data-theme="light"]) .ip-mark--light { display: none; }

#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: var(--ip-font-base);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	color: var(--ip-nav-fg) !important;
}
/* The ellipsis goes on the label itself: on the link it never fired, the text
   being inside a span that would sooner widen the row than shrink. Every list
   here is flex-nowrap (Header.php) for the same reason — Bootstrap's .nav wraps,
   and a wrapping column is as wide as its widest row, so "Industry connections"
   pushed every nested row past its heading's edge and into the scrollbar. */
#sidebar .nav-link > .flex-grow-1 {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* The leading icon, dropped a pixel: lowercase text sits below the middle of its
   line box, so an icon centred on the row reads as riding high beside it. */
#sidebar .nav-link > .bi:first-child {
	position: relative;
	top: var(--ip-icon-nudge);
	/* One box for every icon, whatever the glyph's own width: they differ by a
	   few pixels, which left the labels beside them ragged. */
	flex: none;
	width: 1.25em;
	text-align: center;
	font-size: var(--ip-font-base);
	line-height: 1;
}
/* 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 {
	color: var(--ip-ink) !important;
}
#sidebar .nav-link.active {
	background-color: var(--ip-teal);
	color: var(--ip-on-accent) !important;
}
/* A section heading holding the current page is tinted, not filled. Filled, it
   was a second solid accent pill directly above the page's own — two "you are
   here"s, with the brighter of them on the heading rather than the page. Teal
   text still marks the section when it is closed and the page is out of sight. */
#sidebar button.nav-link.active {
	background-color: var(--ip-mint);
	color: var(--ip-teal) !important;
	font-weight: 600;
}
#sidebar button.nav-link.active:hover { background-color: var(--ip-mint-deep); }

/* The roll-up count beside a nav entry. Mono, being a figure. It used to be a
   Bootstrap .badge, which on the active pill inverted into a solid block of
   on-accent — near-black on the dark theme's light teal, the heaviest thing in
   the sidebar for the least important number on it. */
/* Both centred as boxes rather than set on the text baseline: an inline-block
   on the baseline carried the descender's room under it, and sat visibly low
   against the label and the chevron. */
.nav-count { display: inline-flex; align-items: center; align-self: center; }
.nav-count__n {
	position: relative;
	top: var(--ip-icon-nudge);
	display: inline-flex;
	align-items: center;
	height: 19px;
	margin-left: 8px;
	padding: 0 7px;
	border-radius: 999px;
	background: var(--ip-rule);
	color: var(--ip-muted);
	font-family: var(--ip-mono);
	font-size: var(--ip-font-xs);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	line-height: 1;
}
/* On a pill it takes the pill's own text colour, washed, so it reads as part
   of the entry rather than a patch stuck on it. */
#sidebar .nav-link.active .nav-count__n {
	background: color-mix(in srgb, currentColor 16%, transparent);
	color: inherit;
}

/* Collapsed, the chevron points down; open, it flips up. It takes the
   heading's own colour rather than a fixed one — a fixed ink stayed light on
   the dark theme's teal pill while the label beside it turned dark. */
[data-bs-toggle="collapse"] .sidebar-chevron {
	/* Centred on its own box like the leading icon, so it rides the label's middle. */
	display: inline-flex;
	align-items: center;
	line-height: 1;
	position: relative;
	top: var(--ip-icon-nudge);
	color: currentColor;
	opacity: 0.55;
	transition: opacity 0.18s ease;
	font-size: 0.85em;
}
/* Open, the up chevron's own glyph, upright — not the down one left turned
   over. Text upright is drawn snapped to the pixel grid and text turned is
   not, so a chevron resting at 180° sat a fraction of a pixel off the label's
   middle where the closed one did not. The turn still plays, but only while it
   is turning (impact-shell.js, initSidebarChevrons), ending on this glyph. */
[data-bs-toggle="collapse"][aria-expanded="true"] .sidebar-chevron { opacity: 0.85; }
[data-bs-toggle="collapse"][aria-expanded="true"] .sidebar-chevron::before { content: "\f286"; }
@media (prefers-reduced-motion: reduce) {
	[data-bs-toggle="collapse"] .sidebar-chevron { transition: none; }
}
/* Every other arrow in the app: the sidebar's chevron (`bi-chevron-down`), and
   `.is-up` turns it the way an open sidebar group turns its own. Never a
   Unicode triangle or arrow — those are drawn from whatever font has the
   glyph, at that font's weight and size, and match nothing around them. */
.ip-chevron {
	display: inline-block;
	line-height: 1;
	transition: transform 0.18s ease;
}
.ip-chevron.is-up { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
	.ip-chevron { transition: none; }
}
/* Less room on the right, where a nested row has no chevron to keep clear of:
   indented, the longest label is otherwise a few pixels short of fitting, and
   lost its last letters to an ellipsis whenever the scrollbar showed. */
#sidebar ul ul .nav-link { font-size: var(--ip-font-base); padding-right: 0.5rem; }

/* A line that names the entries under it ("Logging", "Utilities"), the name set
   into the line where the entries' icons start. The two rules are the plain
   divider's own — 1px of the nav's colour at a quarter — so a named line and a
   plain one are the same line. The label is the settings menu's group-label
   type, the same question asked in the same words. */
.sidebar-section {
	display: flex;
	align-items: center;
	margin: 4px 0;
	color: var(--ip-nav-fg);
}
.sidebar-section::before,
.sidebar-section::after {
	content: '';
	border-top: var(--bs-border-width, 1px) solid currentColor;
	opacity: 0.25;
}
/* The lead runs to where the icons start, less the gap the label keeps. */
.sidebar-section::before { flex: none; width: calc(var(--bs-nav-link-padding-x, 1rem) - 6px); }
.sidebar-section::after { flex: 1 1 0; min-width: 12px; }
.sidebar-section__label {
	padding: 0 6px;
	font-size: var(--ip-font-xs);
	font-weight: 600;
	line-height: 1.2;
	color: var(--ip-faint);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* 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: var(--ip-font-h2);
	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;
	/* Fades at whichever end has more menu past it (impact-shell.js sets the
	   attributes). A mask reads only alpha, so the opaque stop can be any
	   opaque token; the lengths are registered so the fades ease in and out
	   rather than snapping. */
	--ip-fade-top: 0px;
	--ip-fade-bottom: 0px;
	mask-image: linear-gradient(to bottom,
		transparent 0,
		var(--ip-surface) var(--ip-fade-top),
		var(--ip-surface) calc(100% - var(--ip-fade-bottom)),
		transparent 100%);
}
/* Eased only once the menu has settled (impact-shell.js sets data-fade-ready).
   Opening the slide-over resizes the list while it is still sliding in, and an
   animated mask over that repaints the brand and the foot beside it every frame —
   a flicker that shows against the dark ground. */
.sidebar-nav-wrapper[data-fade-ready] {
	transition: --ip-fade-top 160ms ease-out, --ip-fade-bottom 160ms ease-out;
}
.sidebar-nav-wrapper[data-more-above] { --ip-fade-top: 28px; }
.sidebar-nav-wrapper[data-more-below] { --ip-fade-bottom: 28px; }
@property --ip-fade-top    { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --ip-fade-bottom { syntax: '<length>'; inherits: false; initial-value: 0px; }

/* 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 {
	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: var(--ip-font-base);
	transition: background 0.15s ease;
	cursor: pointer;
}
/* Sign out. Bootstrap's .text-danger was its own red rather than ours. */
.account-item.account-item--danger,
.account-item.account-item--danger:hover { color: var(--ip-danger); }

/* 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; }
}

/* 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;
}
/* The launch screen (Splash.php). Hidden unless the head script found a cold
   start and set html.ip-splash-on, so with no JavaScript, or storage that
   throws, the page is simply there. Above everything, toasts and dialogs
   included: nothing can be open yet, and anything that tried to would be
   drawn over a page that is not ready. */
.ip-splash {
	display: none;
	position: fixed;
	inset: 0;
	z-index: 10000;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 28px;
	padding: 24px;
	background: var(--ip-bg);
	color: var(--ip-ink);
	text-align: center;
	/* One turn of the mark; the lettering and the organisation both arrive on the
	   second, so these are shared by everything the splash animates. */
	--splash-turn: 800ms;
	--splash-ease: cubic-bezier(0.65, 0, 0.35, 1);
}
html.ip-splash-on .ip-splash,
.ip-splash.is-leaving { display: flex; }
.ip-splash.is-leaving {
	opacity: 0;
	pointer-events: none;
	transition: opacity 250ms ease-out;
}
/* The mark is a ring with one open arc and a centre dot, so turning the whole
   thing reads as the arc travelling round. Eased per turn rather than linear:
   a constant speed looks like a stuck GIF, and this one breathes. */
.ip-splash__mark img {
	width: var(--splash-h);
	height: var(--splash-h);
	animation: ip-splash-spin var(--splash-turn) var(--splash-ease) infinite;
}
@keyframes ip-splash-spin { to { transform: rotate(360deg); } }
/* The brand's horizontal lockup, in its own proportions: the icon is as tall as
   the lettering and the gap is a quarter of that. Sized from the viewport so
   the lettering, 7× as wide as it is tall, still fits a phone.
   The mark starts alone and centred: the first turn is just the spin, and the
   second is the lettering being uncovered. Both run the same duration and curve,
   so the text leaves exactly as the arc comes round. The lockup stays centred,
   so as the text grows the mark is carried left. Splash.php's MIN_MS has to
   outlast the two turns, or the screen is dismissed mid-slide. */
.ip-splash__brand {
	--splash-h: clamp(34px, 8vw, 60px);
	display: flex;
	align-items: center;
}
.ip-splash__brand img { display: block; }
/* The lockup is laid out finished and never reflows. Growing the lettering's
   width would re-run layout every frame on the main thread, which on a page
   still parsing scripts and fonts is exactly when it stutters; transforms run
   on the compositor and cannot be held up by it. The mark starts at the
   lockup's centre and is carried left by half of what the lettering adds; the
   lettering is carried with it, and its wrapper (a window with overflow hidden)
   moves differently from the lettering inside it, so the window's right edge
   grows away from the mark as the lettering is uncovered. */
.ip-splash__brand {
	--splash-text-w: calc(var(--splash-h) * 7.05 + 1px);
	--splash-text-gap: calc(var(--splash-h) * 0.25);
}
.ip-splash__mark {
	animation: ip-splash-carry var(--splash-turn) var(--splash-ease) var(--splash-turn) backwards;
	will-change: transform;
}
@keyframes ip-splash-carry {
	from { transform: translateX(calc((var(--splash-text-w) + var(--splash-text-gap)) / 2)); }
}
.ip-splash__text {
	overflow: hidden;
	/* 564:80 is the lettering's own ratio; the extra pixel keeps rounding from
	   clipping its last stroke. */
	width: var(--splash-text-w);
	margin-left: var(--splash-text-gap);
	animation: ip-splash-window var(--splash-turn) var(--splash-ease) var(--splash-turn) backwards;
	will-change: transform;
}
.ip-splash__text > img {
	height: var(--splash-h);
	width: auto;
	max-width: none;
	animation: ip-splash-counter var(--splash-turn) var(--splash-ease) var(--splash-turn) backwards;
	will-change: transform;
}
/* The lettering travels with the mark, a gap behind it, rather than being
   uncovered where it will end up (the mark crosses that spot on its way left).
   The lettering's own offset is the mark's, W + gap over two; its window is that
   less W, which leaves the window's left edge at the mark and its right edge
   growing from it. */
@keyframes ip-splash-window {
	from { transform: translateX(calc((var(--splash-text-gap) - var(--splash-text-w)) / 2)); }
}
/* By the window's width, not the lettering's own (100%): the window is a pixel
   wider, so 100% left that pixel of the O showing beside the mark for the
   whole first turn, and the lettering a pixel behind the mark. */
@keyframes ip-splash-counter { from { transform: translateX(var(--splash-text-w)); } }
.ip-splash__org {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 14px;
	max-width: min(88vw, 520px);
	animation: ip-rise-in var(--splash-turn) var(--splash-ease) var(--splash-turn) both;
}
.ip-splash__org img { height: 56px; width: auto; max-width: 100%; object-fit: contain; }
.ip-splash__org span {
	font-size: clamp(1.25rem, 4vw, 1.75rem);
	font-weight: 700;
	line-height: 1.2;
	color: var(--ip-ink);
	overflow-wrap: anywhere;
}
.ip-splash__slow {
	display: none;
	align-items: center;
	gap: 10px;
	font-size: var(--ip-font-sm);
	color: var(--ip-muted);
}
.ip-splash.is-slow:not(.is-leaving) .ip-splash__slow { display: flex; }
@media (prefers-reduced-motion: reduce) {
	/* A turning mark is the loading indicator, not decoration, so it is not
	   removed — it is slowed to a pulse that carries no travel. */
	.ip-splash__mark img { animation: ip-splash-pulse 1600ms ease-in-out infinite; }
	.ip-splash__org { animation: none; }
	.ip-splash__text,
	.ip-splash__text > img,
	.ip-splash__mark { animation: none; }
	.ip-splash.is-leaving { transition: none; }
}
@keyframes ip-splash-pulse { 50% { opacity: 0.45; } }

/* 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. Never <body>:
   the background is drawn on it and stays put — see pageParts(). */
html.full-page-transition-pending #main-content,
html.full-page-transition-pending body > .minimal-page-main,
html.full-page-transition-pending body > .ip-help { 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;
	/* --toast-lift: how far it rises to clear a footer (bootstrap-helpers.js). */
	translate: 0 calc(-1 * var(--toast-lift, 0px));
	transition: translate 400ms cubic-bezier(0.32, 0.72, 0, 1);
}
@media (prefers-reduced-motion: reduce) {
	.app-toasts { transition: none; }
}
.app-toasts .ip-toast { pointer-events: auto; }

/* The deck the toasts sit in — showToast() in bootstrap-helpers.js places each
   card by an inline transform and height, and these transitions move it there.
   The box is as tall as nothing while folded, the cards hanging up out of it;
   fanned out it spans them all and takes the pointer, so the gaps between cards
   are part of it and crossing one does not fold the deck back up. */
#toast-container {
	position: relative;
	width: 340px;
	max-width: calc(100vw - 32px);
}
#toast-container[data-expanded] { pointer-events: auto; }

/* The toast. A raised surface like a menu, with the tone carried by the icon
   alone, so a deck of them reads as one family rather than a column of
   coloured headers. Scaled from its top edge, so each card behind shows the
   same strip above the one in front however much it has shrunk. */
.ip-toast {
	position: absolute;
	right: 0;
	bottom: 0;
	width: 100%;
	display: flex;
	align-items: flex-start;
	gap: 10px;
	padding: 12px 12px 12px 14px;
	overflow: hidden;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	box-shadow: var(--ip-shadow-pop);
	color: var(--ip-ink);
	font-size: var(--ip-font-sm);
	transform-origin: 50% 0;
	transition: transform 400ms cubic-bezier(0.32, 0.72, 0, 1),
	            height    400ms cubic-bezier(0.32, 0.72, 0, 1),
	            opacity   400ms ease;
}
.ip-toast > * { transition: opacity 400ms ease; }
/* Folded, a card behind is a strip of edge, not a second message — the top of
   its text showing above the front card read as a garbled line. */
#toast-container:not([data-expanded]) .ip-toast.is-behind > * { opacity: 0; }
/* Fourth and older: kept, and timing out, but not drawn until one in front goes. */
.ip-toast.is-entering,
.ip-toast.is-buried { opacity: 0; pointer-events: none; }
.ip-toast.is-leaving { opacity: 0; pointer-events: none; transition-duration: 300ms; }
@media (prefers-reduced-motion: reduce) {
	.ip-toast { transition-property: opacity; }
}
.ip-toast__icon { font-size: var(--ip-font-base); line-height: 1.25; flex: 0 0 auto; }
.ip-toast--success .ip-toast__icon { color: var(--ip-teal); }
.ip-toast--error   .ip-toast__icon { color: var(--ip-danger); }
.ip-toast--warning .ip-toast__icon { color: var(--ip-amber); }
.ip-toast--info    .ip-toast__icon { color: var(--ip-blue); }
.ip-toast__text { flex: 1 1 auto; min-width: 0; }
.ip-toast__title { font-weight: 600; line-height: 1.35; }
.ip-toast__body { color: var(--ip-label); line-height: 1.45; margin-top: 1px; overflow-wrap: anywhere; }
.ip-toast__close { flex: 0 0 auto; width: 0.6em; height: 0.6em; margin: 4px 0 0 4px; }
/* A toast for work under way (showProgressToast): a thin bar, and the count
   under it in figures that do not jostle as they change. */
.ip-toast__meter { height: 4px; margin-top: 9px; }
.ip-toast__meter .ip-meter__fill { transition: width 120ms linear; }
.ip-toast__meta {
	margin-top: 5px;
	font-family: var(--ip-mono);
	font-size: var(--ip-font-xs);
	font-variant-numeric: tabular-nums;
	color: var(--ip-faint);
}

/* Theme toggle, and the notification bell beside it — icon buttons
   (.ip-btn.ip-btn--icon) sized to the bar's one height. The button gives the box;
   these say only what the bar changes: the quieter border its other controls
   wear, and a label-grey glyph. Scoped to the bar so they outrank .ip-btn, which
   is drawn further down the sheet. */
.app-topbar__tools .ip-btn--icon {
	width: var(--ip-control-h);
	height: var(--ip-control-h);
}
.app-topbar__tools .ip-theme-toggle,
.app-topbar__tools .ip-notif__toggle {
	border-color: var(--ip-border);
	/* The glyph is .ip-icon-scale of this, so a step up the scale, not the
	   compact-control size: alone in a 41px box 12px of icon read as lost. */
	font-size: var(--ip-font-h3);
	color: var(--ip-label);
}
/* The bar's glyphs take .ip-btn's own nudge (--ip-icon-nudge), like every other
   icon button. They once opted out of it, on the reasoning that with no label it
   only pushed the glyph low; but the glyphs draw high in their em box, so the
   opt-out left them reading as high in a box that was centred to the pixel. */
/* The theme toggle's glyph is the destination: a sun only while it is dark. */
:root[data-theme="light"] .ip-theme-toggle__to-light,
:root:not([data-theme="light"]) .ip-theme-toggle__to-dark { display: none; }

/* The bell's wrapper is the dropdown the panel hangs from. */
.ip-notif { display: flex; flex: 0 0 auto; }
.ip-notif__toggle { position: relative; }
.ip-notif__count {
	position: absolute;
	top: -5px;
	right: -5px;
	min-width: 16px;
	height: 16px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--ip-teal);
	color: var(--ip-on-accent);
	/* A ring in the surface the bell sits on, so the badge reads as cut out of it. */
	box-shadow: 0 0 0 2px var(--ip-surface);
	font-family: var(--ip-mono);
	font-size: var(--ip-font-xs);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	line-height: 16px;
	text-align: center;
	pointer-events: none;
}
.ip-notif__count[hidden] { display: none; }

/* The panel is a .dropdown-menu, so Bootstrap places it (Popper keeps it on the
   screen, a phone included) and shows it with .show; this only dresses it. */
.ip-notif__panel {
	width: 340px;
	max-width: calc(100vw - 24px);
	flex-direction: column;
	overflow: hidden;
	padding: 0;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	box-shadow: var(--ip-shadow-pop);
	color: var(--ip-ink);
	font-size: var(--ip-font-sm);
	text-align: left;
}
/* .dropdown-menu is display:none until .show; a flex column only once it is. */
.ip-notif__panel.show { display: flex; }
/* A phone: centred under the bar rather than hung off the bell, where Popper
   put it flush with the right edge and the panel read as something that had
   slipped. Popper writes its position inline, hence !important. Measured from
   --ip-topbar-h, not 100%: the bar's backdrop filter makes it the box a fixed
   child is placed in, and without the filter (reduced transparency) the
   screen is — the bar is at the top of the screen either way. */
@media (max-width: 575.98px) {
	.ip-notif__panel.show {
		position: fixed !important;
		inset: calc(var(--ip-topbar-h, 62px) + 8px) 0 auto 0 !important;
		margin: 0 auto !important;
		transform: none !important;
		width: min(420px, calc(100vw - 24px));
		max-width: none;
	}
}

.ip-notif__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 12px 14px;
	border-bottom: 1px solid var(--ip-border-soft);
}
.ip-notif__title {
	margin: 0;
	font-size: var(--ip-font-base);
	font-weight: 600;
	line-height: 1.3;
	color: var(--ip-ink);
}
.ip-notif__all {
	background: none;
	border: 0;
	border-radius: 6px;
	padding: 4px 6px;
	font: inherit;
	font-size: var(--ip-font-sm);
	font-weight: 600;
	color: var(--ip-teal);
	cursor: pointer;
}
.ip-notif__actions { display: flex; align-items: center; gap: 2px; }
/* The sound switch is the same quiet control as "Mark all read", with a glyph in
   place of words, and the muted colour while it is on: it is a setting, not the
   action the panel is for. */
.ip-notif__sound { color: var(--ip-label); font-size: var(--ip-font-base); line-height: 1; padding: 5px 7px; }
.ip-notif__sound[aria-pressed="false"] { color: var(--ip-faint); }
.ip-notif__all:disabled { color: var(--ip-faint); cursor: default; }
.ip-notif__all:not(:disabled):hover { background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)); }
@media (pointer: coarse) {
	.ip-notif__all { min-height: 44px; }
}

.ip-notif__list {
	list-style: none;
	margin: 0;
	padding: 4px;
	max-height: min(60vh, 420px);
	overflow-y: auto;
	overscroll-behavior: contain;
}
.ip-notif__item {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 2px;
	width: 100%;
	padding: 10px 12px 10px 24px;
	border: 0;
	border-radius: 8px;
	background: none;
	font: inherit;
	color: var(--ip-label);
	text-align: left;
	cursor: pointer;
}
/* The dot on an unread entry is drawn, not a glyph: it has to sit in the same
   place whatever the text does on the line beside it. It is centred on the
   first line of text (the item's top padding plus half that line, less half the
   dot), not on the item, which would leave it between the text and the time and,
   for a notification that wraps, between two lines of the text. */
.ip-notif__item.is-unread { color: var(--ip-ink); }
.ip-notif__item.is-unread::before {
	content: '';
	position: absolute;
	left: 11px;
	top: calc(10px + (var(--ip-font-sm) * 1.4 - 6px) / 2);
	width: 6px;
	height: 6px;
	border-radius: 999px;
	background: var(--ip-teal);
}
.ip-notif__text { font-size: var(--ip-font-sm); line-height: 1.4; overflow-wrap: anywhere; }
.ip-notif__time { font-size: var(--ip-font-xs); color: var(--ip-faint); }
.ip-notif__empty {
	padding: 28px 16px;
	text-align: center;
	color: var(--ip-faint);
	font-size: var(--ip-font-sm);
}

/* "Get these on your phone" — the bell's last line, while this device has a
   step left before it can show notifications with the portal closed
   (notify-setup.js). Below the list rather than in the header, which already
   holds two controls and is the width of a phone. */
.ip-notif__setup {
	display: flex;
	align-items: center;
	gap: 10px;
	width: 100%;
	padding: 12px 14px;
	border: 0;
	border-top: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	font: inherit;
	font-size: var(--ip-font-sm);
	font-weight: 600;
	color: var(--ip-teal);
	text-align: left;
	cursor: pointer;
}
.ip-notif__setup[hidden] { display: none; }
.ip-notif__setup:hover { background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)); }
.ip-notif__setup > span { flex: 1; }
.ip-notif__setup-go { color: var(--ip-faint); }
@media (pointer: coarse) {
	.ip-notif__setup { min-height: 48px; }
}

/* Numbered how-to steps — the notifications guide (notify-setup.js). The
   number is drawn as a disc so a step that wraps keeps its text in a column. */
.ip-howto {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 16px;
}
.ip-howto__step { display: flex; gap: 12px; align-items: flex-start; }
.ip-howto__n {
	flex: 0 0 26px;
	height: 26px;
	display: grid;
	place-items: center;
	border-radius: 999px;
	background: var(--ip-mint);
	color: var(--ip-teal);
	font-size: var(--ip-font-sm);
	font-weight: 700;
}
.ip-howto__text { min-width: 0; padding-top: 2px; }
.ip-howto__title { font-weight: 600; line-height: 1.4; }
.ip-howto__title .bi { color: var(--ip-teal); margin-left: 2px; }
.ip-howto__body { margin-top: 3px; font-size: var(--ip-font-sm); color: var(--ip-muted); line-height: 1.5; }
.ip-howto__body .bi { color: var(--ip-label); }
.ip-howto__body b, .ip-howto__title b { color: var(--ip-ink); font-weight: 600; }
.ip-howto__optional {
	margin-left: 6px;
	font-size: var(--ip-font-xs);
	font-weight: 500;
	color: var(--ip-faint);
}
.ip-howto__action { margin-top: 8px; }
.ip-howto__note { margin: 0; line-height: 1.5; color: var(--ip-label); }
.ip-howto__note .bi { color: var(--ip-teal); margin-right: 4px; }
.ip-howto__aside { margin: 0; font-size: var(--ip-font-xs); color: var(--ip-faint); line-height: 1.5; }
/* Its buttons sit at the right, and a status line ("Waiting for your
   browser…", or why it failed) takes a row of its own above them: beside two
   buttons on a phone it was a column one word wide. */
.ip-modal--setup .ip-modal__foot { flex-wrap: wrap; justify-content: flex-end; }
.ip-modal--setup .ip-modal__hint { flex: 1 0 100%; }
.ip-modal--setup .ip-modal__hint[hidden] { display: none; }

/* "Show me how" under the notifications setting's hint, on Profile. */
.ip-setting__main > .ip-btn--link { margin-top: 6px; }

/* -----------------------------------------------------------------------------
   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); }
/* `clip`, not `hidden`: both round the corners off what is inside, but `hidden`
   also makes the card a scroll container, and a table header inside would
   stick to the card — which never scrolls — instead of to the page.
   The edge is drawn over the card rather than as its border. A clip stops at
   the padding box, so nothing inside could ever paint over a real border —
   and a stuck table header has to, to cut the card off cleanly where it sits
   (see .ip-table__head[data-stuck]). No padding: content runs to the card's
   edge and the edge is drawn over it. A 1px inset here once left a ring of the
   card's own white between the edge and any tinted band inside — a table
   header, a month group — which read as a doubled border. */
.ip-card--flush { padding: 0; border: 0; overflow: clip; position: relative; }
.ip-card--flush::after {
	content: '';
	position: absolute;
	inset: 0;
	border: 1px solid var(--ip-border);
	border-radius: inherit;
	pointer-events: none;
	z-index: 1;
}
.ip-card--flush.ip-card--sunk::after { border-color: var(--ip-border-soft); }
.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;
}
/* A head with nothing under it — a notice that is only a title and a line — has no
   body for the divider to separate from. */
.ip-card__head:only-child { border-bottom: 0; }
/* A head whose text is a paragraph, with an action beside it: the text keeps at
   least 24rem and the action drops under it on a phone, instead of squeezing the
   paragraph into a column a few words wide. */
.ip-card__head--wrap { flex-wrap: wrap; }
.ip-card__head--wrap > :first-child { flex: 1 1 24rem; }
.ip-card__title { font-size: var(--ip-font-h3); font-weight: 600; }
.ip-card__meta { font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.5; text-wrap: pretty; }
.ip-card__title + .ip-card__meta { margin-top: 3px; }

/* A label over a value or a group, in sentence case like everything else that
   is read. Capitals are kept for column headings (.ip-table__head), which
   name a column rather than say something; as a label they were decoration,
   and coloured they shouted. */
.ip-eyebrow {
	font-size: var(--ip-font-xs);
	font-weight: 600;
	color: var(--ip-muted);
}

.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);
}
/* A file's name, shortened from the middle so its extension stays (fileNameHtml
   in impact.js). The base gives way first; the extension never does. */
.ip-file-name { display: inline-flex; min-width: 0; max-width: 100%; white-space: nowrap; }
.ip-file-name__base { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ip-file-name__ext { flex: 0 0 auto; }

/* An attached file as a row (fileRow in form-modal.js): the name takes the room
   that is left and shortens; the size, the tag and the buttons never wrap or
   squeeze, so a long name cannot push Remove off the edge of the dialog. */
.fm-file-row { display: flex; align-items: center; gap: 8px; padding-left: 0; padding-right: 0; min-width: 0; }
.fm-file-row > * { flex: 0 0 auto; }
.fm-file-row > .ip-file-name { flex: 0 1 auto; }
.fm-file-row__size { font-size: var(--ip-font-xs); color: var(--ip-muted); white-space: nowrap; }
.fm-file-row__end { margin-left: auto; display: flex; align-items: center; gap: 12px; padding-left: 8px; }
.fm-file-row.is-removed .ip-file-name { text-decoration: line-through; color: var(--ip-muted); }

/* A file field in a dialog (form-modal.js): the zone is the picker. The input
   is kept focusable but out of sight, and its focus ring is drawn on the button
   that is its label. */
.fm-dropzone { position: relative; padding: 20px; transition: border-color 0.15s, background-color 0.15s; }
.fm-dropzone.is-dragover { border-color: var(--ip-teal); background: var(--ip-mint); }
.fm-dropzone.is-disabled { opacity: 0.6; }
.fm-dropzone.is-disabled .ip-btn { pointer-events: none; }
.fm-dropzone__input {
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}
.fm-dropzone__input:focus-visible + .ip-btn { outline: 2px solid var(--ip-focus); outline-offset: 2px; }
.ip-dropzone__hint {
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	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: 0.5em;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	padding: 10px 15px;
	font-size: var(--ip-font-base);
	font-weight: 500;
	cursor: pointer;
	color: var(--ip-ink);
	text-decoration: none;
}
/* Bootstrap Icons nudge their glyph down by .125em to sit on a text baseline,
   which in a flex button reads as an icon hanging below the label's middle.
   Centred on the box instead, then dropped a pixel: lowercase text sits below the
   middle of its line box, so a truly centred icon reads as high. */
.ip-btn > .bi { display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
.ip-btn > .bi::before { vertical-align: 0; }
.ip-btn > .bi { position: relative; top: var(--ip-icon-nudge); font-size: var(--ip-icon-scale); }
/* Every button's plus is this one glyph, so they match. It is drawn hairline-thin
   beside 600-weight text; a stroke of its own
   colour thickens it without growing it. */
.ip-btn > .bi-plus-lg::before { -webkit-text-stroke: var(--ip-icon-stroke) currentColor; }
.ip-btn:hover { 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); }

/* A band of light that sweeps across the button once on hover — for the one
   button that sells something. Drawn from the accent's own ink colour so it
   follows both themes; resting position is off the left edge, so nothing shows
   until the pointer arrives. */
.ip-btn--shine { position: relative; overflow: hidden; isolation: isolate; }
.ip-btn--shine::after {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	width: 45%;
	transform: translateX(-130%) skewX(-20deg);
	background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ip-shine, var(--ip-on-accent)) 38%, transparent), transparent);
	pointer-events: none;
}
/* While the pointer stays, the sweep repeats every 3s: it crosses in the first
   1.4s and rests for the remainder. Leaving stops it where it is, off the
   button's edge or not — the resting transform above takes over at once. */
.ip-btn--shine:hover:not(:disabled)::after,
.ip-btn--shine:focus-visible::after {
	animation: ip-shine 3s ease-in-out infinite;
}
@keyframes ip-shine {
	0%   { transform: translateX(-130%) skewX(-20deg); }
	47%, 100% { transform: translateX(330%) skewX(-20deg); }
}
@media (prefers-reduced-motion: reduce) {
	.ip-btn--shine::after { display: none; }
}

.ip-btn[hidden],
.ip-tag[hidden] { display: none; }
/* The violet every upgrade prompt wears (.ip-plan-lock, the Upgrade tags), as a
   full-size button. Pairs with .ip-btn--shine, whose band takes the violet. */
.ip-btn--upgrade {
	--ip-shine: var(--ip-violet);
	background: var(--ip-violet-bg);
	color: var(--ip-violet);
	border-color: color-mix(in srgb, var(--ip-violet) 35%, transparent);
	font-weight: 600;
}
.ip-btn--upgrade:hover {
	background: var(--ip-violet-bg);
	color: var(--ip-violet);
	border-color: var(--ip-violet);
}

.ip-btn--block { width: 100%; }
.ip-btn--sm { padding: 6px 11px; font-size: var(--ip-font-sm); }
/* Icon only, square — a row's own action beside its text, where a label would
   take the room the text needs. Always with an aria-label and a title. */
.ip-btn--icon { padding: 0; width: 30px; height: 30px; flex: 0 0 auto; }
/* Transparent, so it takes the colour of the ground it sits on — a sunk footer, a
   tinted panel — rather than standing on it as a white chip. Everything else is
   the default button's: on a card the two are identical. Its border used to be
   one step fainter (--ip-border, not --ip-border-input), which beside a default
   button read as a mistake rather than as anything meant. */
.ip-btn--quiet { background: transparent; }
/* 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: var(--ip-font-sm);
	padding: 0;
	text-decoration: underline;
}
.ip-btn--link:hover { background: transparent; color: var(--ip-ink); }
/* A quiet Remove beside an item. Written as a pair so it stays a link — the
   outlined danger button is for a destructive action that stands alone. */
.ip-btn--link.ip-btn--danger { color: var(--ip-danger); }
.ip-btn--link.ip-btn--danger:hover { background: transparent; color: var(--ip-danger-dark); }

/* The asterisk after a required field's label. */
.ip-req { color: var(--ip-danger); margin-left: 4px; }

.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: var(--ip-font-sm);
	font-weight: 500;
	cursor: pointer;
	background: transparent;
	color: var(--ip-label);
	white-space: nowrap;
	/* Centred by the box, not by padding and line-height, so a control given a
	   height (the bar's, the map's) centres its labels without restating this. */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* Above the pill, which is a sibling drawn behind it. */
	position: relative;
	z-index: 1;
	transition: color 200ms ease;
}
.ip-seg__btn.is-active { background: var(--ip-teal); color: var(--ip-on-accent); }
/* A count written as " · 14" after the label. The button is a flex box, which
   makes the span its own item, and an item's leading space is collapsed away
   ("Approvals· 14"). Preserved here, once, so no caller has to know. */
.ip-seg__btn > [data-count] { white-space: pre; }

/* 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: var(--ip-font-xs);
	color: var(--ip-faint);
	font-weight: 600;
}

/* 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: var(--ip-font-xs);
	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: var(--ip-font-sm); }

/* Read-only tag. A program's tag (.ip-prog, under "Programs") is the same
   thing drawn in FIRST's colours, so the shape is one rule for both — two
   rules had drifted into a pill beside a square badge. The outline is the
   tag's own colour, thinned, so every tone and both themes get a matching
   edge without a token each; the padding gives back its pixel. */
.ip-tag,
.ip-prog {
	font-size: var(--ip-font-xs);
	font-weight: 500;
	/* A whole pixel, and an odd height overall (17 + 4 + 2 = 23px). At 1.35
	   the tag was 22.2px tall, so centring it in the top bar's 41px team button
	   put its box on a fraction of a pixel; the letters are drawn on whole
	   pixels, and sat visibly low in the pill. Odd in odd centres exactly. */
	line-height: 1.4;
	line-height: round(1.4em, 1px);
	padding: 2px 7px;
	border: 1px solid color-mix(in srgb, currentColor 32%, transparent);
	border-radius: 999px;
	white-space: nowrap;
}
.ip-tag {
	display: inline-block;
	background: var(--ip-rule);
	color: var(--ip-muted);
}
.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--green  { background: var(--ip-green-bg);  color: var(--ip-green); }
.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); }
/* A state that has gone wrong — an expired invitation, errors on a dialog tab.
   Not a sixth hue for categories: it means something needs seeing to. */
.ip-tag--danger { background: var(--ip-danger-bg); color: var(--ip-danger); }

/* A feature the plan does not include — a lock and "Upgrade" (planLock() in
   impact.js). Violet, the one hue no status uses, so it is never read as a
   state the record is in. */
/* Relative so its visually hidden label ("Locked, needs the Impact plan") is
   placed against the tag. Without a positioned ancestor it measured from the
   page: in a sidebar long enough to scroll, it hung below the window and made
   the whole document 109px taller than the screen. */
.ip-plan-lock { position: relative; display: inline-flex; align-items: center; gap: 4px; }
/* The icon's size and colour are set under "Plan gate", after the menus whose
   own icon rules would otherwise repaint it. */

/* A control in its locked form (lockControl()): muted, so it does not read as
   the action it names, with its pill. The button is what is clicked — through
   to the plans — so the pill inside it stays a tag. */
.ip-btn.is-locked { color: var(--ip-muted); }
.ip-btn.is-locked .ip-plan-lock { margin-left: 1px; }

/* 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: var(--ip-font-xs);
	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. */
/* Only where a pointer can hover. A tap on a touch screen leaves :hover applied to
   whatever was tapped until the next tap elsewhere, so the lift stayed up. */
@media (hover: hover) {
	.ip-cycle:hover { box-shadow: inset 0 0 0 40px var(--ip-veil-hover); transform: translateY(-1px); }
}
.ip-cycle:active { transform: translateY(0); box-shadow: inset 0 0 0 40px var(--ip-veil-hover); }
.ip-cycle__glyph { font-size: var(--ip-font-xs); 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: var(--ip-font-xs);
	color: var(--ip-muted);
}
.ip-cycle-hint__glyph { font-size: var(--ip-font-xs); 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: var(--ip-font-xs); font-weight: 600; color: var(--ip-label); }
.ip-field__hint  { font-size: var(--ip-font-xs); color: var(--ip-faint); }

.ip-input {
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	padding: 10px 12px;
	font-size: var(--ip-font-sm);
	/* A full-size button's line, not this text's own: the button sets its label at
	   the base size, so a field at the smaller size came out 3px shorter than the
	   button beside it (see .ip-btn). The compact pair below share a size and
	   need no help. */
	line-height: calc(var(--ip-font-base) * 1.5);
	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: var(--ip-font-xs); color: var(--ip-danger); }
.ip-input--sm { padding: 6px 8px; font-size: var(--ip-font-sm); border-radius: 9px; line-height: 1.5; }
.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: var(--ip-font-sm);
	color: var(--ip-faint);
	pointer-events: none;
}
.ip-search > .ip-input { padding-left: 34px; }
/* In a toolbar the label is stretched to the row, and the small input inside it
   would stay at its own 35px at the top of that height — visibly high beside a
   taller segmented control. It fills the label instead, so the box is exactly
   as tall as whatever else is in the row. */
.ip-toolbar > .ip-search { display: flex; }
.ip-toolbar > .ip-search > .ip-input { height: auto; flex: 1; }

/* 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; }
/* Under the search box on a line of its own, whatever is in it. Left to share the
   line, four placeholder chips fitted beside the search and the ten real ones did
   not, so the row dropped a line when they arrived and everything under it moved. */
.ip-toolbar__filters--row { flex: 1 1 100%; }
/* On a phone the chips are one row that scrolls sideways. Wrapped, ten of them
   took five rows, and the first entry of the list started two-thirds of the way
   down the screen. Full-bleed to the card's edges so the row visibly runs on. */
@media (max-width: 640px) {
	.ip-toolbar__filters {
		flex-wrap: nowrap;
		flex-basis: 100%;
		overflow-x: auto;
		margin-inline: -14px;
		padding-inline: 14px;
		scrollbar-width: none;
		overscroll-behavior-x: contain;
	}
	.ip-toolbar__filters::-webkit-scrollbar { display: none; }
	.ip-toolbar__filters > * { flex: none; }
}
/* The same row of chips without a toolbar round it (the calendar's, which sit on
   the page itself): one line that scrolls sideways on a phone, and a line of its
   own above whatever else shares the row. */
.ip-chiprow { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; min-width: 0; }
@media (max-width: 640px) {
	.ip-chiprow {
		flex: 1 0 100%;
		flex-wrap: nowrap;
		overflow-x: auto;
		scrollbar-width: none;
		overscroll-behavior-x: contain;
	}
	.ip-chiprow::-webkit-scrollbar { display: none; }
	.ip-chiprow > * { flex: none; }
}
/* The calendar's: types are the window's own, so there can be ten, and wrapped
   beside the subscribe button the last one sat alone on a second line with the
   button hanging off the first. One line that scrolls, as on a phone, with the
   button kept where it is — and the row the same height before the chips arrive
   and after, so the calendar does not drop when they do. */
.ip-chiprow--scroll {
	flex-wrap: nowrap;
	overflow-x: auto;
	scrollbar-width: none;
	overscroll-behavior-x: contain;
	padding-right: 24px;
	-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
	mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.ip-chiprow--scroll::-webkit-scrollbar { display: none; }
.ip-chiprow--scroll > * { flex: none; }
/* Centred against its own line, unlike the rest of the row: the toolbar
   stretches its items to the height of a control, and a line of text stretched
   that way sits at the top of the box, level with nothing beside it. */
.ip-toolbar__meta { align-self: center; white-space: nowrap; font-size: var(--ip-font-xs); 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; }
/* A wrapper holding a count or note is stretched to the row's height like the
   rest of the toolbar, so what is in it is centred in that height itself. */
div.ip-toolbar__end { display: flex; align-items: center; }

/* Footnote — the quiet line under a list saying how it behaves. */
.ip-footnote { font-size: var(--ip-font-xs); 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: var(--ip-font-sm);
	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: var(--ip-font-xs);
	font-weight: 500;
	white-space: nowrap;
}
/* The wrapper a picker redraws its tokens into. `contents` hands the tokens to
   the control's flex row, so its gap falls between them too — as a box of its
   own the wrapper took the gap, and the tokens inside it sat edge to edge. */
.ip-combo__tokens { display: contents; }
.ip-combo__token button {
	border: 0;
	background: transparent;
	color: inherit;
	cursor: pointer;
	font-size: var(--ip-font-sm);
	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: var(--ip-font-sm);
	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-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)); }
.ip-combo__option.is-on { color: var(--ip-teal); font-weight: 600; }
.ip-combo__option__mark { width: 14px; flex: 0 0 14px; font-size: var(--ip-font-xs); }
.ip-combo__option__sub { margin-left: auto; font-size: var(--ip-font-xs); color: var(--ip-faint); }

.ip-combo__empty { padding: 12px 10px; font-size: var(--ip-font-sm); 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: var(--ip-font-sm);
	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: var(--ip-font-xs);
	padding: 0;
}
.ip-check.is-on { border-color: var(--ip-teal); background: var(--ip-teal); }
/* An on/off setting. A button with role="switch" rather than a checkbox, so it
   acts at once like the "Turn on" button it replaced. The track takes the
   buttons' 9px corner and the thumb 3px less, so the gap all round is even. */
.ip-switch {
	position: relative;
	flex: 0 0 44px;
	width: 44px;
	height: 26px;
	padding: 0;
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	background: var(--ip-surface);
	cursor: pointer;
	transition: background-color 150ms ease, border-color 150ms ease;
}
.ip-switch::after {
	content: "";
	position: absolute;
	top: 3px;
	left: 3px;
	width: 18px;
	height: 18px;
	border-radius: 6px;
	background: var(--ip-faint);
	transition: transform 150ms ease, background-color 150ms ease;
}
.ip-switch[aria-checked="true"] { background: var(--ip-teal); border-color: var(--ip-teal); }
.ip-switch[aria-checked="true"]::after { transform: translateX(18px); background: var(--ip-on-accent); }
.ip-switch:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ip-mint); }
.ip-switch:disabled { opacity: 0.5; cursor: default; }
/* Working: the change is being saved, or the state is still being read. The
   thumb rests half way and breathes, and the track keeps showing the setting as
   it stands. The page ignores clicks while it lasts instead of using :disabled,
   which would dim the switch as though it were unavailable. */
.ip-switch[aria-busy="true"] { cursor: progress; }
.ip-switch[aria-busy="true"]::after { transform: translateX(9px); animation: ip-switch-busy 900ms ease-in-out infinite; }
@keyframes ip-switch-busy { 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) {
	.ip-switch, .ip-switch::after { transition: none; }
	.ip-switch[aria-busy="true"]::after { animation: none; }
}
.ip-check--sm { width: 17px; height: 17px; flex-basis: 17px; border-radius: 4px; font-size: var(--ip-font-xs); }

/* -----------------------------------------------------------------------------
   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: var(--ip-font-xs);
	font-weight: 600;
}
.ip-avatar--lg { width: 28px; height: 28px; flex-basis: 28px; font-size: var(--ip-font-xs); }

/* 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; }

/* Two cards share the tile grid: the person's to-dos and the leadership
   pipeline. Each is the full width until the band is wide enough to hold them
   beside the tiles. */
.ip-kpis__todos,
.ip-kpis__pipeline { 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)); } }
/* Five columns: the six tiles in three across and two down at the left, the
   pipeline under them, and the to-dos the full height of all three at the right.
   The to-dos are placed first and the pipeline beside them, so the tiles — which
   are left to flow — fill what is free: the first three cells of each of the two
   top rows. Stretched, so the list is as tall as the block it stands beside. */
@container (min-width: 880px) {
	.ip-kpis { grid-template-columns: repeat(5, minmax(0, 1fr)); }
	.ip-kpis__todos { grid-column: 4 / span 2; grid-row: 1 / span 3; align-self: stretch; }
	.ip-kpis__pipeline { grid-column: 1 / span 3; grid-row: 3; }
}

/* Tight, because they sit among cards that have more to say: the band is two
   tile rows and a card, and every pixel a tile does not need is a pixel the
   pipeline under them does not have to give up. */
.ip-kpi {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	padding: 12px 14px 10px;
	box-shadow: var(--ip-shadow-sm);
}
.ip-kpi__label { font-size: var(--ip-font-xs); color: var(--ip-muted); font-weight: 500; }
.ip-kpi__value {
	font-size: var(--ip-font-h1);
	font-variant-numeric: tabular-nums;
	font-weight: 600;
	letter-spacing: -0.03em;
	margin-top: 4px;
	line-height: 1;
}
.ip-kpi__delta { font-size: var(--ip-font-xs); color: var(--ip-teal); margin-top: 4px; font-weight: 500; }

/* Compact stat tiles above a list */
/* The compact figures above a list: each a slim tile, its label and its figure
   on one line, so a row of them costs one line of height above the table they
   summarise rather than two.

   How many share a line is chosen from how many there are and how wide the row
   is, never left to wrapping: plain wrapping put four tiles three and one. So
   four go 4, then 2 × 2; five go 5, then 3 + 2; three go 3 or stack; anything
   stacks on a phone. Each tile is about 200px at its narrowest — below that the
   label wraps beside its figure. The row is the container, so a settings
   column and a full page each lay out by their own width. */
.ip-stats {
	container-type: inline-size;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}
.ip-stat {
	--per: 1;
	flex: 0 0 auto;
	width: calc((100% - (var(--per) - 1) * 12px) / var(--per));
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 11px;
	padding: 10px 14px;
	min-width: 0;
}
.ip-stat__label { font-size: var(--ip-font-sm); color: var(--ip-muted); min-width: 0; }
/* Counted among visible tiles only (`of .ip-stat:not([hidden])`): a tile shown
   only once there is something in it (StatTiles' `optional`) is in the row from
   the start, hidden, and counting it laid three tiles out as four. Counting
   tiles, not children, also steps over the hidden status <span> the page
   skeleton puts ahead of them. */
/* Two 200px tiles and a gap. Three stack here rather than go two and one. */
@container (min-width: 412px) {
	.ip-stat { --per: 2; }
	.ip-stats:has(> .ip-stat:nth-child(3 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat,
	.ip-stats:has(> .ip-stat:nth-child(5 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat:nth-last-child(1 of .ip-stat:not([hidden])) { --per: 1; }
}
/* Three. Five go three and two; four stay two by two. */
@container (min-width: 624px) {
	.ip-stats:has(> .ip-stat:nth-child(3 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat,
	.ip-stats:has(> .ip-stat:nth-child(5 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat:nth-child(-n+3 of .ip-stat:not([hidden])) { --per: 3; }
	.ip-stats:has(> .ip-stat:nth-child(5 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat:nth-child(n+4 of .ip-stat:not([hidden])) { --per: 2; }
}
@container (min-width: 848px) {
	.ip-stats:has(> .ip-stat:nth-child(4 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat { --per: 4; }
}
/* `:nth-child(n of …)` matches every visible tile; it is there to outweigh the
   three-and-two rules above, which carry one more pseudo-class than a bare
   `.ip-stat`. */
@container (min-width: 1048px) {
	.ip-stats:has(> .ip-stat:nth-child(5 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden]))) > .ip-stat:nth-child(n of .ip-stat:not([hidden])) { --per: 5; }
}
/* A lone tile is the whole row at any width. */
.ip-stats > .ip-stat:nth-child(1 of .ip-stat:not([hidden])):nth-last-child(1 of .ip-stat:not([hidden])) { --per: 1; }
.ip-stats[hidden],
.ip-stat[hidden],
.ip-kpis__tiles[hidden],
.ip-kpi[hidden] { display: none; }
.ip-stat__value { font-size: var(--ip-font-h3); font-weight: 600; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A bare figure over its label, inside a card that is already the tile — the
   award plan's counts, the risk register's, the dashboard's pipeline. Three
   screens drew this by hand at 20, 22 and 24px with three label spacings, so
   the same kind of number changed size from page to page. */
.ip-figure__value {
	font-size: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
}
.ip-figure__label { font-size: var(--ip-font-xs); color: var(--ip-muted); margin-top: 6px; line-height: 1.35; }

/* A figure whose colour is its meaning: something overdue, something done. */
.ip-stat__value.is-danger, .ip-figure__value.is-danger { color: var(--ip-danger); }
.ip-stat__value.is-good,   .ip-figure__value.is-good   { color: var(--ip-teal); }

/* 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; }
/* Many months: narrower gaps so the bars keep some width, and the few labels
   left are allowed to run past their own bar — they mark year starts, with
   empty bars either side to run into. */
.ip-bars--dense { gap: 3px; }
.ip-bars--dense .ip-bar__label { overflow: visible; }

.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: var(--ip-font-xs); color: var(--ip-muted); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }
.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: var(--ip-font-xs); 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); }

/* One item of the Learning page's season plan: the wording, then its meter. The
   meter column has a floor, so on a phone it left the wording about 80px — a
   word a line — and the stacked form is the one that reads. */
.ip-planrow {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(190px, 240px);
	gap: 20px;
	align-items: start;
	padding: 16px 18px;
	border-top: 1px solid var(--ip-rule);
}
@media (max-width: 640px) {
	.ip-planrow { grid-template-columns: minmax(0, 1fr); gap: 12px; }
}

/* The risk matrix's likelihood labels sit under five columns about 35px wide on a
   phone, so "Unlikely" and "Possible" ran into each other; read down the column
   instead, like the Impact label beside the grid. */
.ip-matrix-xlabel { font-size: var(--ip-font-xs); color: var(--ip-faint); text-align: center; line-height: 1.3; }
@media (max-width: 640px) {
	.ip-matrix-xlabel { writing-mode: vertical-rl; white-space: nowrap; text-align: start; justify-self: center; }
}

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

/* Charts grow into place once they are well in view — the landing page's
   motion, timing and curve, so the product draws its charts the way the page
   that sells it does. impact.js marks a chart .is-waiting before it is first
   painted and lifts the mark when the chart is seen; `--i` is each column's or
   row's place and `--step` the gap between them. A bar's figure fades in as the
   bar reaches it, rather than waiting over an empty column.
   Screen only: a report printed before its last chart was scrolled to must not
   print it empty. */
@media screen and (prefers-reduced-motion: no-preference) {
	.ip-bar__fill {
		transform-origin: bottom;
		transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--i, 0) * var(--step, 45ms) + 80ms);
	}
	.ip-bar__value { transition: opacity 400ms ease-out calc(var(--i, 0) * var(--step, 45ms) + 380ms); }
	.ip-bars.is-waiting .ip-bar__fill { transform: scaleY(0); }
	.ip-bars.is-waiting .ip-bar__value { opacity: 0; }

	.ip-meters .ip-meter__fill {
		transform-origin: left;
		transition: transform 1100ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--i, 0) * var(--step, 60ms) + 100ms);
	}
	.ip-meters.is-waiting .ip-meter__fill { transform: scaleX(0); }
}

/* Key/value list */
.ip-kv { display: flex; justify-content: space-between; gap: 10px; font-size: var(--ip-font-sm); }
.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); font-variant-numeric: tabular-nums; }

/* 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: var(--ip-font-xs);
	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: var(--ip-font-sm);
	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;
}
/* Every row lights under the pointer, so the eye can follow a wide row across
   its columns. Only the pointer cursor says the row can be opened. */
.ip-table__row:hover { text-decoration: none; color: inherit; }
.ip-table__row.is-clickable { cursor: pointer; }
/* Drawn inside the row: the table scrolls sideways, and a scroller clips
   anything painted outside its edges — the ring's left and right sides with it. */
.ip-table__row:focus-visible { outline-offset: -2px; background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)); }

/* The due time and reminder on a to-do are tags with a glyph in front. */
.ip-todo .ip-tag > .bi { margin-right: 4px; }

/* A to-do row (My to-dos) is an .ip-table__row whose title is the one text that
   changes state: a finished item keeps its place in the list, muted and struck
   through, so the list reads as what is left plus what is done. A flagged item's
   title is a link, so it takes the ink colour rather than the accent. */
.ip-todo__title { min-width: 0; overflow-wrap: anywhere; color: var(--ip-ink); }

/* The to-do sheet's form: a label over its control, with the hint and the error
   under it. Outside a dialog's grid the label is not a block by default. */
#todo-sheet .ip-field__label { display: block; margin-bottom: 6px; }
#todo-sheet .ip-field__hint,
#todo-sheet .invalid-feedback { margin-top: 6px; }
a.ip-todo__title { text-decoration: none; }
a.ip-todo__title:hover { text-decoration: underline; }
.ip-todo.is-done .ip-todo__title { color: var(--ip-faint); text-decoration: line-through; }
/* The tick is the Getting started tick (.ip-start__tick): a round 22px mark,
   so the two lists a person ticks off read as one control. Scoped to the row
   because .ip-check is the square box everywhere else. */
.ip-todo > .ip-check { width: 22px; height: 22px; flex-basis: 22px; border-radius: 999px; border-color: var(--ip-border-input); }
.ip-todo > .ip-check.is-on { border-color: var(--ip-teal); }

/* The dashboard inbox's tabs are a segmented control in a card head. They take
   the whole row on a phone, where three labels with counts can still be wider
   than the card, and then scroll sideways rather than being cut off. */
#dash-inbox-tabs { min-width: 0; max-width: 100%; }
#dash-inbox-tabs .ip-seg { overflow-x: auto; scrollbar-width: none; max-width: 100%; }

/* A to-do row is a tick box, a line and its buttons — the same three things at
   every width — so it keeps its grid on a phone. The generic .ip-table__row
   stacks its cells there, which is right for columns of figures and wrong for
   this: the tick box became a bar across the row and the buttons dropped under
   the title. Two classes beat that rule's one; the columns are the row's own
   inline template. */
@media (max-width: 640px) {
	.ip-table__row.ip-todo {
		display: grid;
		align-items: center;
		gap: 12px;
		padding: 12px 18px;
	}
}

/* The to-do toggle is an icon button beside .ip-btn--sm text buttons, and
   .ip-btn--icon is a fixed 30px: a small text button is padding + border + one
   line of text, which is 33-35px depending on the font and line height, so the
   square sat visibly shorter than Delete and Edit beside it. Sized from the
   same line instead, so it tracks them. `:where` keeps this at the specificity
   of .ip-btn--icon itself, so the phone's larger touch size still wins. */
:where(.ip-todo-toggle).ip-btn--icon { width: calc(1lh + 14px); height: calc(1lh + 14px); }
/* The same, for a row's actions where a small text button (Done, Start) sits
   beside its icon buttons: a 30px square beside a 34px button reads as a
   mistake. Opt-in on the group, so a row of icons alone keeps its 30px. */
:where(.ip-actions-line) .ip-btn--icon { width: calc(1lh + 14px); height: calc(1lh + 14px); }

/* A header that stays while a long list scrolls under it. Only on a table that
   fits its card — table-layout.js decides, and says why there — and only in a
   flush card, whose edges the table reaches.
   It stops a page's padding below the bar, not against it, so the list keeps
   the same air under the bar that it had before it scrolled. */
.ip-table[data-fits] { overflow-x: visible; }
/* Under the card's drawn edge (z 1) at rest, so the edge shows over it; above
   it once stuck, where it has to cover what has scrolled behind it — and then
   draws the edge itself, top and sides, since it now sits over the card's. */
.ip-card--flush .ip-table[data-fits] > .ip-table__head {
	position: sticky;
	top: var(--ip-stick-top);
	z-index: 0;
}
.ip-card--flush .ip-table[data-fits] > .ip-table__head[data-stuck] { z-index: 2; }
/* While it is stuck, the rows it has passed are still there above it, under
   the gap and the bar. The header carries its own cover for them: a band of
   page above it, and the card's rounded top corners drawn over its own square
   ones. Both are part of the header, so they move with it on the compositor —
   a clip set from a scroll handler trailed it by a frame, and a fast scroll
   showed rows sliding over the top edge. Only *becoming* stuck waits on
   script, and at that moment there are no rows above the header yet. */
.ip-table__head[data-stuck] {
	box-shadow:
		inset 0 1px 0 var(--ip-border),
		inset 1px 0 0 var(--ip-border),
		inset -1px 0 0 var(--ip-border);
}
.ip-table__head[data-stuck]::before,
.ip-table__head[data-stuck]::after {
	content: '';
	position: absolute;
	left: 0;
	right: 0;
	pointer-events: none;
}
.ip-table__head[data-stuck]::before {
	bottom: 100%;
	height: var(--ip-stick-top);
	background-color: var(--ip-canvas);
}
:root[data-theme="dark"] .ip-table__head[data-stuck]::before {
	/* The page's own grid, pinned to the viewport the way it is, so the band
	   lines up with what surrounds it instead of reading as a patch. */
	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;
}
/* The corners: page outside the curve, the card's edge on it, the header's
   own fill inside. Same 12px radius as the card. */
.ip-table__head[data-stuck]::after {
	top: 0;
	height: 12px;
	background:
		radial-gradient(circle at 100% 100%, transparent 10.5px, var(--ip-border) 11px 11.5px, var(--ip-canvas) 12.5px) left top / 12px 12px no-repeat,
		radial-gradient(circle at 0 100%, transparent 10.5px, var(--ip-border) 11px 11.5px, var(--ip-canvas) 12.5px) right top / 12px 12px no-repeat;
}

/* A row is a card wherever the table cannot hold its columns: on a phone
   always, and at any wider width where the table's own floor is more than its
   card has room for (table-layout.js sets data-stack). It is what the grid
   becomes instead of a sideways scroll: scrolling a 1000px grid through a
   360px window hid the name the moment you moved, and at a laptop's width it
   pushed the reviewer's buttons out of sight. What the card holds: what it is
   about across the top, each other figure under its own column name in even
   columns, and its buttons along the bottom. The columns are fixed rather
   than wrapped to fit: wrapping left every card a different ragged shape,
   with a figure stranded beside the buttons. */
.ip-table[data-stack] .ip-table__head { display: none; }
.ip-table[data-stack] .ip-table__row,
.ip-table[data-stack] .ip-table__group {
	/* The inline floor is the grid's. A stacked row has no columns to keep. */
	min-width: 0 !important;
}
/* Not a to-do row, which keeps its own columns (below). The template is
   forced because desktop's comes from .ip-table--cols or the row's own
   style attribute (the dashboard's), and neither applies to a card. */
.ip-table[data-stack] .ip-table__row:not(.ip-todo) {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
	align-items: start;
	gap: 10px 16px;
	padding: 14px 16px;
}
.ip-table[data-stack] .ip-table__row > * { min-width: 0; max-width: 100%; }
.ip-table[data-stack] .ip-table__row:not(.ip-todo) > .ip-cell-lead,
.ip-table[data-stack] .ip-table__row:not(.ip-todo, :has(> .ip-cell-lead)) > :first-child { order: -1; grid-column: 1 / -1; }
/* A row of buttons is what the card is for, not one more figure: it takes
   the width under them. A lone Edit fits a column and stays in one. */
.ip-table[data-stack] .ip-table__row:not(.ip-todo) > :last-child:has(.ip-btn + .ip-btn) { order: 1; grid-column: 1 / -1; }
.ip-table[data-stack] .ip-table__row:not(.ip-todo) > :last-child:has(.ip-btn + .ip-btn) > .ip-row { justify-content: flex-start !important; flex-wrap: wrap; }
.ip-table[data-stack] .ip-table__row > [data-blank] { display: none; }
.ip-table[data-stack] .ip-table__row:not(.ip-todo) > .ip-meter { grid-column: 1 / -1; align-self: center; }
/* Nothing hovers on a phone to scroll a clipped cell into view, and a card
   has height to spare, so what was cut short to keep a row one line wraps. */
.ip-table[data-stack] .ip-table__row :is(.ip-cell-strong, .ip-cell-clip) { white-space: normal; overflow-wrap: anywhere; }
.ip-table[data-stack] .ip-table__row .ip-cell-right,
.ip-table[data-stack] .ip-table__row .ip-cell-center { text-align: left; }
.ip-table[data-stack] .ip-table__row > [data-label]::before {
	content: attr(data-label);
	display: block;
	font-size: var(--ip-font-xs);
	font-weight: 600;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--ip-faint);
	margin-bottom: 2px;
	/* A cell that is itself a flex row (an avatar and a name) would otherwise
	   seat this label beside them and leave the name a few letters wide. */
	flex: 0 0 100%;
}
.ip-table[data-stack] .ip-table__row > .ip-cell-who[data-label] { flex-wrap: wrap; row-gap: 0; }
/* The lead says what the row is; a heading over it would say "Name". The
   meter is drawn by its fill, which a label would push aside. */
.ip-table[data-stack] .ip-table__row > .ip-cell-lead::before,
.ip-table[data-stack] .ip-table__row:not(:has(> .ip-cell-lead)) > :first-child::before,
.ip-table[data-stack] .ip-table__row > .ip-meter::before { content: none; }
/* Wider than a phone, a card has room for more figures abreast. As many even
   columns as fit, so a laptop's card is two lines deep rather than five. */
@media (min-width: 641px) {
	.ip-table[data-stack] .ip-table__row:not(.ip-todo) { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)) !important; }
}
.ip-table__note { padding: 12px 18px; font-size: var(--ip-font-xs); color: var(--ip-faint); }
/* A table written by DataTable.php: its columns are set once on the table, and
   the headings, the rows and the placeholder rows all read them — so a heading
   cannot sit over a column it does not belong to. */
.ip-table--cols .ip-table__head,
.ip-table--cols .ip-table__row {
	grid-template-columns: var(--ip-cols);
	min-width: var(--ip-table-min, 0);
}
.ip-table--cols .ip-table__group { min-width: var(--ip-table-min, 0); }
/* Placeholder rows are not rows to point at. */
.sk-table-row { pointer-events: none; }
/* An empty state or error under the headings (tableMessage() in impact.js).
   Held to the left edge, so a table wider than its card does not scroll the
   message out of sight. */
.ip-table__message {
	position: sticky;
	left: 0;
	padding: 8px 18px 20px;
}

/* 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); }


/* 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: var(--ip-font-sm); font-weight: 500; line-height: 1.4; }
.ip-approval__meta { font-size: var(--ip-font-xs); 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: var(--ip-font-xs); font-weight: 600; letter-spacing: 0.03em; }
.ip-table__group-stat { font-size: var(--ip-font-xs); color: var(--ip-faint); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }

/* 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; }
/* Under a grid of cards the footer is a row of its own, not one more cell. */
:is(.ip-team-grid, .ip-evidence-grid) > .ip-table__foot { grid-column: 1 / -1; padding: 4px 0 0; }
.ip-table__count { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); 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: var(--ip-font-xs); margin-top: 2px; }
.ip-cell-date { color: var(--ip-faint); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); }
.ip-cell-num { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }
.ip-cell-right { text-align: right; }
.ip-cell-center { text-align: center; }
.ip-cell-muted { color: var(--ip-muted); }
/* A figure with nothing in it. The same quiet dash in every column, whatever
   weight or colour the column's figures take: in a bold column it read as a
   value rather than an absence. */
.ip-cell-none { color: var(--ip-muted); font-weight: 400; }
/* Whether an entry carries one of the things Completeness checks — the
   dashboard's award mark: teal on mint when it does, an amber cross when not. */
.ip-has {
	display: inline-grid;
	place-items: center;
	width: 18px;
	height: 18px;
	border-radius: 50%;
	font-size: var(--ip-font-xs);
	background: var(--ip-mint);
	color: var(--ip-teal);
}
/* What an entry is missing is the cross in its column, in amber, rather than a
   symbol before its name or a tint across the row: a gap is common, and the
   place to read it is down the three columns, which a calm row leaves clear.
   The mark keeps its soft amber disc so it reads at a glance without shouting. */
.ip-has.is-no { background: var(--ip-amber-bg); color: var(--ip-amber-fg); }
/* 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: var(--ip-font-xs);
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* table-layout.js scrolls the rest of a clipped cell into view under the
   pointer; an ellipsis on text that is moving would mark the wrong end. */
.ip-cell-strong.is-revealing,
.ip-cell-clip.is-revealing { text-overflow: clip; }

/* 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: var(--ip-font-sm);
	line-height: 1.6;
}
.ip-empty__title { font-size: var(--ip-font-sm); font-weight: 600; color: var(--ip-muted); margin-bottom: 4px; }
.ip-empty__action { margin-top: 14px; }
.ip-empty__icon { display: block; margin-bottom: 10px; font-size: 1.75rem; color: var(--ip-teal); opacity: 0.8; }

/* The screen tour (tour.js). One element dims the page: the hole is a box over
   the part being shown, and its enormous spread shadow is the scrim around it,
   so the part stays lit and the rest of the screen is one colour. The root
   takes every click, so nothing behind the tour can be pressed by accident. */
.ip-tour { position: fixed; inset: 0; z-index: 1070; }
/* While the phone's sidebar slides in or out for the next stop: the card would
   otherwise sit pointing at where the part was. */
.ip-tour.is-moving .ip-tour__card { visibility: hidden; }
.ip-tour__hole {
	position: fixed;
	border-radius: 10px;
	box-shadow: 0 0 0 9999px var(--ip-scrim), 0 0 0 2px var(--ip-teal);
	pointer-events: none;
	transition: top 260ms ease, left 260ms ease, width 260ms ease, height 260ms ease;
}
.ip-tour__card {
	position: fixed;
	width: min(340px, calc(100vw - 24px));
	padding: 16px 18px 14px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	box-shadow: var(--ip-shadow-pop);
	animation: ip-rise-in 200ms ease both;
}
.ip-tour__step { font-size: var(--ip-font-xs); color: var(--ip-faint); font-weight: 600; }
.ip-tour__title { margin-top: 4px; font-size: var(--ip-font-h3); font-weight: 600; }
.ip-tour__text { margin: 6px 0 0; font-size: var(--ip-font-sm); color: var(--ip-label); line-height: 1.55; }
.ip-tour__foot { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.ip-tour__spacer { flex: 1; }
@media (prefers-reduced-motion: reduce) {
	.ip-tour__hole { transition: none; }
	.ip-tour__card { animation: none; }
}

/* Getting started — the dashboard's first steps for a new account
   (getting-started.js). A card at the top of the dashboard, each step ticked
   from the record, done ones kept in place and quietened so the list shows
   progress rather than shrinking under the reader. */
.ip-start[hidden] { display: none; }
.ip-start__head { display: flex; align-items: flex-start; gap: 12px; }
.ip-start__heading { flex: 1; min-width: 0; }
.ip-start__count { margin-top: 3px; font-size: var(--ip-font-xs); color: var(--ip-muted); }
.ip-start__close { margin: -4px -6px 0 0; border-color: transparent; color: var(--ip-muted); }
.ip-start__bar {
	height: 6px;
	margin: 14px 0 6px;
	border-radius: 999px;
	background: var(--ip-track);
	overflow: hidden;
}
.ip-start__bar > span {
	display: block;
	height: 100%;
	border-radius: inherit;
	background: var(--ip-teal);
	transition: width 400ms ease;
}
.ip-start__steps { list-style: none; margin: 0; padding: 0; }
.ip-start__step {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 12px 0;
	border-top: 1px solid var(--ip-rule);
}
.ip-start__step:first-child { border-top: 0; }
.ip-start__tick {
	flex: 0 0 22px;
	height: 22px;
	display: grid;
	place-items: center;
	border-radius: 999px;
	border: 1.5px solid var(--ip-border-input);
	font-size: var(--ip-font-xs);
}
.ip-start__step.is-done .ip-start__tick { background: var(--ip-teal); border-color: var(--ip-teal); color: var(--ip-on-accent); }
.ip-start__text { flex: 1; min-width: 0; }
.ip-start__title { font-weight: 600; font-size: var(--ip-font-sm); line-height: 1.4; }
.ip-start__step.is-done .ip-start__title { font-weight: 500; color: var(--ip-faint); text-decoration: line-through; }
.ip-start__detail { margin-top: 2px; font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.5; }
.ip-start__step > .ip-btn { flex: 0 0 auto; }
/* A phone: the button drops under its words rather than squeezing them to a
   column, lined up with the text past the tick. */
@media (max-width: 575.98px) {
	.ip-start__step { flex-wrap: wrap; align-items: flex-start; }
	.ip-start__step > .ip-btn { margin-left: 34px; }
}
/* 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: var(--ip-font-xs);
	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: var(--ip-font-sm); 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 field another source is filling in — the add-team dialog's name and
   location while FIRST is asked about the number. The same turning ring as a
   working step, inside the box that will change, because a spinner in the
   dialog's footer says something is happening without saying where. Wrapped
   rather than drawn on the .ip-field so a hint under the input cannot move it. */
.ip-autofill { position: relative; display: block; }
.ip-autofill.is-busy > .ip-input { padding-right: 36px; }
/* A select's arrow sits where the ring turns; the ring is the one that means something meanwhile. */
.ip-autofill.is-busy > select.ip-input { background-image: none; }
.ip-autofill.is-busy::after {
	content: '';
	position: absolute;
	right: 12px;
	top: 50%;
	width: 14px;
	height: 14px;
	margin-top: -7px;
	border-radius: 50%;
	border: 2px solid var(--ip-mint-deep);
	border-top-color: var(--ip-teal);
	animation: ip-progress-turn 800ms linear infinite;
	pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
	.ip-autofill.is-busy::after { 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: var(--ip-font-sm); }
.ip-record__key { flex: 0 0 56px; color: var(--ip-muted); }
.ip-record__val { flex: 1; min-width: 0; font-family: var(--ip-mono); font-variant-numeric: tabular-nums; 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: var(--ip-font-sm);
	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;
	animation: ip-fade-in 280ms ease backwards;
}
.ip-flyout__scrim.is-leaving { animation: ip-fade-out 220ms ease forwards; pointer-events: none; }

.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;
	/* The body scrolls up and down; sideways is the swipe that pushes the sheet
	   away (swipe-dismiss.js). */
	touch-action: pan-y;
	/* In from the edge and back out to it, on the same path. `backwards`, not
	   `both`: a fill that outlived the entrance would hold the sheet at rest and
	   override the offset a finger drags it to. */
	animation: ip-sheet-in 320ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}
/* Only a `to`: it starts from wherever the sheet is — at rest after the close
   button, or part-way off after a swipe — rather than snapping back to rest first. */
.ip-flyout.is-leaving { animation: ip-sheet-out 220ms cubic-bezier(0.4, 0, 1, 1) forwards; pointer-events: none; }
@keyframes ip-sheet-in  { from { transform: translateX(100%); } }
@keyframes ip-sheet-out { to   { transform: translateX(100%); } }
@keyframes ip-fade-in   { from { opacity: 0; } }
@keyframes ip-fade-out  { to   { opacity: 0; } }

/* 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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.015em; margin-top: 10px; line-height: 1.3; }
.ip-flyout__sub   { font-size: var(--ip-font-sm); 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: var(--ip-font-sm);
}

.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: var(--ip-font-xs); 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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
.ip-modal__sub { font-size: var(--ip-font-sm); 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: var(--ip-font-xs);
	color: var(--ip-faint);
}
.ip-modal__hint.is-error { color: var(--ip-danger); }
/* On a phone the hint takes the line above the buttons: beside them it was left a
   column of about five words wide. */
@media (max-width: 640px) {
	.ip-modal__foot { flex-wrap: wrap; justify-content: flex-end; }
	.ip-modal__hint { flex: 1 0 100%; }
	.ip-modal__hint[hidden] { display: none; }
}
/* 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: var(--ip-font-sm);
	font-weight: 600;
	cursor: pointer;
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
}
.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; }

/* flex-shrink: 0 because this is a flex item of <body>, and an explicit
   min-height replaces the automatic one that stops a flex item shrinking below
   its content. It shrank to the viewport instead, and on a short laptop screen
   the centred form spilled out of both ends — the top half above the page,
   where no scrolling reaches, taking the heading with it. */
.minimal-page-main {
	flex-shrink: 0;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: center;
	/* The bottom clears the Help button pinned in the corner, so the last
	   line can always be scrolled out from under it. */
	padding: 48px 16px 80px;
}

.ip-auth { width: 100%; max-width: 400px; margin: 0 auto; }
.ip-auth__card { padding: 28px 28px 24px; }
.ip-auth__title { font-size: var(--ip-font-h1); font-weight: 600; letter-spacing: -0.02em; line-height: 1.25; margin: 0 0 18px; }
.ip-auth__sub { font-size: var(--ip-font-sm); 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: var(--ip-font-xs); }
.ip-auth__remember { display: flex; gap: 10px; align-items: flex-start; font-size: var(--ip-font-sm); 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: var(--ip-font-sm); color: var(--ip-muted); margin: 18px 0 0; }
/* The other ways to finish the second step of signing in, under the one on offer. */
.ip-auth__ways { list-style: none; padding: 0; margin: 18px 0 0; display: grid; gap: 8px; font-size: var(--ip-font-sm); text-align: center; }
/* A line of what an assistant will be allowed, on the OAuth consent page: the
   mark hangs beside the text, so a wrapped line lines up under its first word. */
.ip-auth__can { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 8px; font-size: var(--ip-font-sm); line-height: 1.5; }
.ip-auth__can > .bi { line-height: 1.5; }
.ip-auth .ip-input:disabled { color: var(--ip-muted); background: var(--ip-surface-sunk); }

/* A password box with its show/hide eye inside the right-hand end. The box
   keeps room for the eye whether or not it is drawn, so the text never runs
   under it and nothing shifts when the script shows it. */
.ip-auth__secret { position: relative; }
.ip-auth__secret .ip-input { padding-right: 42px; }
.ip-auth__reveal {
	position: absolute;
	top: 50%;
	right: 5px;
	transform: translateY(-50%);
	width: 32px;
	height: 32px;
	display: grid;
	place-items: center;
	padding: 0;
	border: 0;
	border-radius: 7px;
	background: transparent;
	color: var(--ip-muted);
	cursor: pointer;
}
.ip-auth__reveal:hover { color: var(--ip-ink); }
.ip-auth__reveal:focus-visible { color: var(--ip-ink); }
.ip-auth__reveal[hidden] { display: none; }

/* Lines sign-in.js writes under a password box — Caps Lock, and the confirm
   box not matching. Empty, and taking no room in the field's gap, until then. */
.ip-auth__caps { color: var(--ip-amber); }
.ip-auth__match { color: var(--ip-danger); }
.ip-auth__caps:empty,
.ip-auth__match:empty { display: none; }

.ip-wordmark {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 8px;
	margin-top: 40px;
	font-size: var(--ip-font-sm);
	color: var(--ip-muted);
}
.ip-wordmark img { width: 200px; max-width: 60vw; height: auto; }
/* The publisher's credit, not a call to action — it keeps the line's colour. */
.ip-wordmark a { color: inherit; }
.ip-wordmark a:hover { color: var(--ip-ink); }

/* 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: var(--ip-font-h1); font-weight: 600; letter-spacing: -0.02em; margin: 0; }
.ip-docs__sub { font-size: var(--ip-font-sm); 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-variant-numeric: tabular-nums;
	font-size: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--ip-ink);
}
.ip-tz__meta { font-size: var(--ip-font-xs); 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;
}
/* `minmax(0, …)` as above, not a bare 1fr: 1fr's floor is its content's width, and
   fourteen month labels laid end to end made the column 476px on a 390px phone —
   the page scrolled sideways and the hours on the right were cut off.
   Stacked up to 1200px rather than 900px: beside the sidebar a half-width card
   is under 480px there, and fourteen month names set in it run into each other
   ("Aug '25Sep Oct Nov Dec Jan '26Feb"). */
@media (max-width: 1200px) { .ip-dash-split { grid-template-columns: minmax(0, 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; }
/* min-width: 0 on both, or a flex item's minimum is its content's width — every
   bar's label laid end to end — and a long window pushes the chart out of its
   card and under the one beside it. */
.ip-dash-split #dash-months { flex: 1; display: flex; min-width: 0; }
/* min-height, because the stretch is only ever up to the card beside it: on a
   phone, where there is no card beside it, and beside a short list of meters,
   height: auto left the bars nothing to be a percentage of — a row of 4px
   stubs, with the peak's figure sitting on its neighbours. */
.ip-dash-split #dash-months .ip-bars { flex: 1; height: auto; min-height: 190px; min-width: 0; }

/* min() so a phone narrower than one card gets one full-width column, not a
   340px card with its score and figures clipped off the right edge. */
/* An evidence note under an award criterion: its first line, opening into the
   whole note with the line breaks it was written with (pages/awards.js). */
.ip-crit-note {
	margin-top: 9px;
	border-left: 2px solid var(--ip-mint-deep);
	padding-left: 10px;
	font-size: var(--ip-font-xs);
	color: var(--ip-muted);
	line-height: 1.5;
}
.ip-crit-note__toggle {
	display: flex;
	align-items: center;
	gap: 7px;
	width: 100%;
	min-width: 0;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	font: inherit;
	text-align: left;
	cursor: pointer;
}
.ip-crit-note__toggle:hover { color: var(--ip-ink-soft); }
.ip-crit-note__toggle .ip-chevron { flex: 0 0 auto; color: var(--ip-faint); }
.ip-crit-note__preview { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ip-crit-note__body { padding: 6px 0 2px 19px; white-space: pre-wrap; overflow-wrap: anywhere; }

.ip-award-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 16px; }
.ip-award { padding: 20px; }
.ip-award__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.ip-award__name { font-size: var(--ip-font-h3); font-weight: 600; }
.ip-award__window { font-size: var(--ip-font-xs); color: var(--ip-muted); margin-top: 3px; }
.ip-award__score {
	font-size: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
	color: var(--ip-amber);
}
.ip-award__score.is-good { color: var(--ip-teal); }
.ip-award__of { font-size: var(--ip-font-sm); font-weight: 500; letter-spacing: 0; color: var(--ip-faint); }
.ip-award__checks {
	display: flex;
	flex-direction: column;
	gap: 9px;
	margin-top: 16px;
	padding-top: 16px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-award__check { display: flex; align-items: flex-start; gap: 9px; font-size: var(--ip-font-sm); }
.ip-award__label { flex: 1; line-height: 1.45; }
.ip-award__stat { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); color: var(--ip-faint); white-space: nowrap; }
.ip-award__mark {
	width: 16px;
	height: 16px;
	flex: 0 0 16px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	font-size: var(--ip-font-xs);
	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); }
/* Not graded yet: the dot is there, so the row does not shift when it is. */
.ip-award__mark.is-pending { background: var(--ip-skeleton-base); }

/* -----------------------------------------------------------------------------
   Shared lists — the organisation'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: var(--ip-font-sm);
	font-family: inherit;
	cursor: pointer;
}
.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-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); color: var(--ip-faint); }

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

/* One line per option: six lists of six are scanned far more than edited, so
   the name and its description share a row rather than stacking. */
.ip-lists__item {
	display: flex;
	align-items: center;
	gap: 8px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 9px;
	padding: 4px 10px 4px 8px;
	background: var(--ip-surface);
}
.ip-lists__fields {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0 4px;
	flex: 1 1 0;
	min-width: 0;
}
.ip-lists__meta {
	display: flex;
	align-items: center;
	gap: 9px;
	flex: 0 0 auto;
	margin-left: auto;
}
/* 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; }

/* The six dots a reorderable row is dragged by (sortable.js). Only the grip
   starts a drag, so the row's own inputs keep selecting text. `touch-action`
   hands the gesture to the page instead of letting the browser scroll with it. */
.ip-grip {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 20px;
	height: 26px;
	background: transparent;
	border: 0;
	border-radius: 6px;
	color: var(--ip-faint);
	font-size: var(--ip-font-base);
	line-height: 1;
	cursor: grab;
	padding: 0;
	margin: 0 -4px 0 -5px;
	touch-action: none;
}
/* Bootstrap Icons sets every glyph a little below the baseline, which leaves
   the dots low in their box — the box is what is centred here. */
.ip-grip .bi::before { display: block; vertical-align: 0; }
.ip-grip:hover:not(:disabled) { color: var(--ip-ink); }
/* A list of one has nowhere to move to; the dots stay so the row lines up
   with the rows it will have, but say nothing. */
.ip-grip:disabled { opacity: 0.35; cursor: default; }

/* Lifted off the list while it travels, and above the rows sliding past it. */
[data-item].is-dragging {
	position: relative;
	z-index: 2;
	box-shadow: var(--ip-shadow-pop);
}
/* For the length of a drag the pointer is a closed hand wherever it wanders,
   and nothing on the page is selected by the sweep. */
.is-sorting,
.is-sorting * { cursor: grabbing !important; user-select: none; }

/* The row's own swatch — the button that opens the presets. Filled with the
   chip's ink for the reason .ip-swatch is. */
.ip-lists__swatch {
	width: 20px;
	height: 20px;
	flex: 0 0 20px;
	border-radius: 6px;
	border: 0;
	background: var(--swatch);
	cursor: pointer;
	padding: 0;
}
.ip-lists__swatch:hover,
.ip-lists__swatch[aria-expanded="true"] { box-shadow: 0 0 0 2px var(--ip-surface), 0 0 0 3px var(--ip-border); }

/* 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: 4px 6px;
	background: transparent;
	color: var(--ip-ink);
	font-family: inherit;
	font-size: var(--ip-font-sm);
	font-weight: 500;
	/* A description longer than its column says so, rather than stopping
	   mid-word as if that were the whole of it. Focus shows the rest. */
	text-overflow: ellipsis;
}
.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;
}
/* A fixed width rather than the text's own, so every row's description starts
   on the same line down the list and reads as a column. */
.ip-lists__edit--name { flex: 0 1 150px; }
.ip-lists__edit--hint {
	flex: 1 1 140px;
	font-size: var(--ip-font-xs);
	font-weight: 400;
	color: var(--ip-muted);
}
.ip-lists__edit--hint::placeholder { color: var(--ip-placeholder); }

.ip-lists__uses { font-size: var(--ip-font-xs); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; 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: var(--ip-font-xs);
	color: var(--ip-muted);
	white-space: nowrap;
	cursor: pointer;
}
.ip-lists__cap input { margin: 0; cursor: pointer; }

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

.ip-lists__note {
	font-size: var(--ip-font-xs);
	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; }
	/* No room for a column: the description drops under the name instead. */
	.ip-lists__item { flex-wrap: wrap; }
	.ip-lists__edit--name,
	.ip-lists__edit--hint { flex: 1 1 100%; }
	.ip-lists__meta { flex-basis: 100%; justify-content: flex-end; }
}

/* -----------------------------------------------------------------------------
   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: center;
	gap: 12px;
	flex-wrap: wrap;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	padding: 10px 14px 10px 8px;
	background: var(--ip-surface);
}
.ip-plan__item.is-off { opacity: 0.55; }
/* One line of text, so the row hugs it like the lists' rows do. */
.ip-plan__item--compact { gap: 8px; padding: 4px 10px 4px 8px; }
.ip-plan__main { flex: 1 1 260px; min-width: 200px; }
.ip-plan__name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* The same cluster as .ip-lists__meta — switch and remove are the lists' own,
   untouched. Only the button is made compact. */
.ip-plan__actions {
	display: flex;
	align-items: center;
	gap: 9px;
	flex: 0 0 auto;
	margin-left: auto;
}
.ip-plan__actions > .ip-btn { padding: 3px 10px; font-size: var(--ip-font-xs); }

/* 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: 64px;
	padding: 6px 8px;
	border-radius: 9px;
	background: var(--ip-surface-sunk);
	border: 1px solid var(--ip-border-soft);
}
.ip-plan__target-num {
	font-size: var(--ip-font-h2);
	font-weight: 650;
	line-height: 1.1;
	color: var(--ip-ink);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
}
.ip-plan__target-word { font-size: var(--ip-font-xs); color: var(--ip-faint); }

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

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

/* Coloured inline from the organisation'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: var(--ip-font-xs);
	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: var(--ip-font-xs); 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-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	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); }

.ip-evidence-row__date { flex: 0 0 auto; font-size: var(--ip-font-xs); color: var(--ip-faint); padding-top: 2px; }
.ip-evidence-row__name { display: block; font-size: var(--ip-font-sm); font-weight: 600; line-height: 1.4; }
.ip-evidence-row__note {
	font-size: var(--ip-font-xs);
	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: var(--ip-font-xs); color: var(--ip-faint); margin-top: 5px; line-height: 1.45; }

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

/* On a phone the reviewer's buttons share the card's width rather than huddling
   at one end: they are the targets a thumb is there for. The bookmark stays
   square. */
@media (max-width: 640px) {
	.ip-approvals__actions > .ip-btn:not(.ip-todo-toggle) { flex: 1 1 0; }
}

/* 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. */
/* 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: var(--ip-font-xs);
	color: var(--ip-amber);
	margin-top: 5px;
	line-height: 1.45;
	text-wrap: pretty;
}

/* Gaps and strengths — one row each, marked like the award checks above them,
   so the same ! and ✓ mean the same thing twice on one screen. */
.ip-next {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 12px 18px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-next:first-child { border-top: 0; }
/* The mark shares a grid with the text rather than the row, so it sits on the
   title's first line whether the row is centred on a taller button or not. */
.ip-next__text {
	flex: 1 1 auto;
	min-width: 0;
	display: grid;
	grid-template-columns: 16px minmax(0, 1fr);
	column-gap: 12px;
	align-items: start;
}
.ip-next__text > .ip-award__mark { margin-top: 2px; }
.ip-next__title { font-size: var(--ip-font-sm); font-weight: 500; line-height: 1.4; }
.ip-next__detail { grid-column: 2; font-size: var(--ip-font-xs); color: var(--ip-muted); margin-top: 2px; }
.ip-next > .ip-btn { flex: 0 0 auto; }

.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: var(--ip-font-h1); font-weight: 600; letter-spacing: -0.02em; }
.ip-program__name { font-size: var(--ip-font-sm); font-weight: 600; }
.ip-program__detail { font-size: var(--ip-font-xs); 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 {
	/* Equal by construction — the indicator above is placed by arithmetic on that.
	   Without the zero floor a long label ("Impact/Inspire") took a wider share and
	   every step after it drifted out from under its underline. */
	flex: 1 1 0;
	min-width: 0;
	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: var(--ip-font-xs);
	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: var(--ip-font-xs); font-weight: 500; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
.ip-step-panel__sub { font-size: var(--ip-font-sm); 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: var(--ip-font-xs); 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: var(--ip-font-base);
	line-height: 1;
}
.ip-type.is-on .ip-type__glyph { background: var(--ip-mint-deep); color: var(--ip-teal); }
.ip-type__name { font-size: var(--ip-font-sm); font-weight: 600; }
.ip-type__hint { font-size: var(--ip-font-xs); 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: var(--ip-font-sm);
	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-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); 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: var(--ip-font-sm); font-weight: 500; }
.ip-member-row__role { font-size: var(--ip-font-xs); color: var(--ip-faint); }
/* The volunteer role picker, in place of the member's own roles on the types
   that ask what each person did (log.js ROLE_TYPES). Narrows on a phone so the
   hours beside it stay on the row. In the day grid it fills its column. */
.ip-member-row__pick { flex: 0 0 auto; width: clamp(96px, 30vw, 140px); }
.ip-attend__row .ip-member-row__pick { width: 100%; }

/* 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: var(--ip-font-xs);
	font-weight: 600;
	color: var(--ip-faint);
}
.ip-attend__name {
	font-size: var(--ip-font-sm);
	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: var(--ip-font-xs); font-weight: 600; color: var(--ip-muted); }
.ip-attend__date { font-size: var(--ip-font-xs); color: var(--ip-faint); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }

/* The cell is the control: a field holding the hours someone was at that
   session, filled teal when they were there, rather than a checkbox with a
   label beside it — the label is the column heading. Empty is "not there". */
.ip-attend__cell {
	width: 52px;
	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: var(--ip-font-xs);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	text-align: center;
	cursor: pointer;
	padding: 0 4px;
	appearance: textfield;
	-moz-appearance: textfield;
}
.ip-attend__cell::-webkit-inner-spin-button,
.ip-attend__cell::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.ip-attend__cell::placeholder { color: var(--ip-faint); }
.ip-attend__cell:focus { outline: none; border-color: var(--ip-teal); box-shadow: 0 0 0 3px var(--ip-mint); }
.ip-attend__cell.is-on {
	border-color: var(--ip-teal);
	background: var(--ip-teal);
	color: var(--ip-on-accent);
	cursor: text;
}
.ip-attend__cell:disabled { opacity: 0.4; cursor: default; }
/* Arrival and leaving instead of hours (log.js showTimes). A present cell is
   the two clock fields stacked, with a button to take the person off; an
   absent one is a button to put them on, shaped like the hours cell. */
.ip-attend__opts {
	display: flex;
	align-items: center;
	gap: 10px;
	margin-bottom: 10px;
	font-size: var(--ip-font-sm);
	color: var(--ip-muted);
}
.ip-attend__times {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 22px;
	grid-template-rows: auto auto;
	gap: 3px 2px;
	align-items: center;
}
.ip-attend__time {
	grid-column: 1;
	min-width: 0;
	height: 26px;
	padding: 0 6px;
	border-radius: 7px;
	border: 1.5px solid var(--ip-teal);
	background: var(--ip-surface);
	color: var(--ip-ink);
	font-size: var(--ip-font-xs);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
}
.ip-attend__time:focus { outline: none; box-shadow: 0 0 0 3px var(--ip-mint); }
.ip-attend__clear {
	grid-column: 2;
	grid-row: 1 / span 2;
	width: 22px;
	height: 22px;
	padding: 0;
	border: 0;
	border-radius: 6px;
	background: transparent;
	color: var(--ip-faint);
	cursor: pointer;
	display: grid;
	place-items: center;
}
.ip-attend__clear:hover { background: var(--ip-rule); color: var(--ip-ink); }
.ip-attend__add {
	width: 52px;
	height: 30px;
	margin: 0 auto;
	display: block;
	border-radius: 8px;
	border: 1.5px solid var(--ip-box-border);
	background: var(--ip-surface);
	color: var(--ip-faint);
	font-size: var(--ip-font-xs);
	cursor: pointer;
}
.ip-attend__add:disabled { opacity: 0.4; cursor: default; }
.ip-member-row__time { flex: 0 0 auto; width: 112px; }
.ip-member-row__dash { color: var(--ip-faint); }
/* On a phone the times go under the name rather than squeezing it to a word. */
@media (max-width: 575.98px) {
	.ip-member-row:has(.ip-member-row__time) { flex-wrap: wrap; }
	.ip-member-row:has(.ip-member-row__time) .ip-member-row__name { flex: 1 1 60%; }
	.ip-member-row__time { flex: 1 1 96px; width: auto; }
}
.ip-member-row__hrs {
	flex: 0 0 auto;
	width: 48px;
	text-align: right;
	font-size: var(--ip-font-xs);
	color: var(--ip-faint);
}
.ip-attend__total {
	text-align: right;
	font-size: var(--ip-font-xs);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	color: var(--ip-muted);
}

/* The sittings of an entry that has several — a row of date, start and length
   each, under one set of headings. The columns are fixed so the rows line up;
   the date takes the slack. */
.ip-sessions { display: flex; flex-direction: column; gap: 6px; }
.ip-sessions__row {
	display: grid;
	grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 0.8fr) 30px;
	gap: 8px;
	align-items: center;
}
.ip-sessions__row--head {
	font-size: var(--ip-font-xs);
	font-weight: 600;
	color: var(--ip-faint);
}
.ip-sessions__foot {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px 12px;
	margin-top: 4px;
}
.ip-attend__note { font-size: var(--ip-font-xs); 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: var(--ip-font-base);
}

.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. The card brings no padding of its own: the head
   row and the body carry it, so a section's number sits the same distance
   from the edge whether it is the fixed first one or a toggle. */
.ip-section { overflow: hidden; padding: 0; }
.ip-section__toggle,
.ip-section__head {
	width: 100%;
	display: flex;
	align-items: center;
	gap: 12px;
	background: transparent;
	border: 0;
	padding: 16px 20px;
	text-align: left;
}
.ip-section__toggle { cursor: pointer; }
.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: var(--ip-font-xs);
	font-weight: 600;
}
.ip-section__mark.is-done { background: var(--ip-mint); color: var(--ip-teal); }
.ip-section__title { flex: 1; font-size: var(--ip-font-sm); font-weight: 600; }
.ip-section__summary { font-size: var(--ip-font-xs); color: var(--ip-muted); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }
.ip-section__caret { font-size: var(--ip-font-xs); color: var(--ip-faint); }
.ip-section__body { padding: 0 20px 20px; }

/* Collapse — anything that opens and closes in place. The grid row runs from
   0fr to 1fr, which the browser can tween where it cannot tween height to
   `auto`; the inner box clips what is not yet shown. The body stays in the
   page while closed so there is something to animate from — mark it `inert`
   then, or Tab walks into fields nobody can see. A body added only when it
   opens appears in a single frame: the cut this exists to replace. */
.ip-collapse {
	display: grid;
	grid-template-rows: 0fr;
	transition: grid-template-rows 220ms cubic-bezier(0.2, 0, 0, 1);
}
.ip-collapse.is-open { grid-template-rows: 1fr; }
.ip-collapse__inner {
	min-height: 0;
	overflow: hidden;
	opacity: 0;
	transition: opacity 160ms ease-out;
}
.ip-collapse.is-open > .ip-collapse__inner { opacity: 1; transition-delay: 40ms; }
@media (prefers-reduced-motion: reduce) {
	.ip-collapse, .ip-collapse__inner { transition: none; }
}

.ip-summary-rail__headline { font-size: var(--ip-font-base); 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: var(--ip-font-xs);
	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; }
}

/* Side panes stay in view while the list beside them scrolls. They held a
   selected record's detail, or the summary of what is being built, and
   scrolled away with the page — so picking a tile far down the evidence
   grid changed a pane nobody could see any more.
   Pinned where a table header pins (--ip-stick-top), and capped to the room
   left below it less the same gap again, scrolling inside itself when taller. Only
   while beside the list: wrapped underneath it, a pinned pane would sit over
   the list it is meant to follow. The wrap points are each layout's two
   flex bases plus the gap, queried against the layout for the same reason
   as the rail caps above. */
.ip-teams-rail,
.ip-people-rail,
.ip-log-rail,
.ip-report-side {
	/* 4px above the line, because the pane's box starts 4px above its card
	   (the shadow room below): pinned at the line itself, the card sat 4px
	   lower than the list beside it, at rest and stuck alike. */
	position: sticky;
	top: calc(var(--ip-stick-top) - 4px);
	max-height: calc(100dvh - var(--ip-stick-top) - 20px);
	overflow-y: auto;
	/* Stated, because overflow-y: auto makes the other axis scroll too: one
	   unbreakable line in a card gave the pane a sideways scrollbar under it.
	   The content is expected to wrap; this only stops a miss becoming a bar. */
	overflow-x: hidden;
	overscroll-behavior: contain;
	/* Room for the cards' shadows, which fall below them and which the
	   scrolling box would otherwise clip, given back so the pane still lines
	   up. Top and bottom only: room at the sides changes the pane's outer
	   width, and with it the width at which the layout wraps. The bottom
	   has 20px, not 4: the pane's cap ends at the viewport's edge, and a
	   card scrolled to its end sat flush against it, looking cut off. */
	padding-block: 4px 20px;
	margin-block: -4px -20px;
}
/* A rail holding one card (a rail of several cards, like People's, has no one
   card to scroll, so it stays the scroller itself).

   The rail is only the column: it takes the height of the row, which the list
   beside it decides — `contain: size` keeps what it holds from lengthening the
   row, so a tall card and a short list (a placeholder, a filter that matches
   two teams) no longer leave the page scrolling past the end of the list. The
   card is what is pinned and what scrolls: as tall as the room under the bar
   less the same 16px gap again, and never taller than the column, so its edge
   rests where the list's does. The card is plain, not flush, so it has no
   `::after` edge that would scroll with the content. */
:is(.ip-teams-rail, .ip-log-rail) {
	position: static;
	align-self: stretch;
	contain: size;
	max-height: none;
	height: auto;
	padding: 0;
	margin: 0;
	overflow: visible;
}
/* The row is never shorter than the window under the bar less the 16px kept
   below the card — so a short list (the placeholders, two matches) still gets a
   full-height card, the same height it has once the list is long, and the page
   still ends exactly at the window's edge. That needs #main-content's bottom
   padding to be the same 16px rather than its usual 40: with 40, the card was
   24px shorter while loading and jumped when the list arrived. */
#main-content:has(:is(.ip-teams-rail, .ip-log-rail)) { padding-bottom: 16px; }
:is(.ip-teams-layout, .ip-log-layout):has(> :is(.ip-teams-rail, .ip-log-rail)) {
	min-height: calc(100dvh - var(--ip-stick-top) - 16px);
}
:is(.ip-teams-rail, .ip-log-rail) > .ip-card {
	position: sticky;
	top: var(--ip-stick-top);
	height: min(calc(100dvh - var(--ip-stick-top) - 16px), 100%);
	overflow-y: auto;
	overscroll-behavior: contain;
}
/* Wrapped under the list, or in the sheet: a block of its own height, with
   nothing pinned and nothing scrolling inside. */
@container (max-width: 787px) {
	.ip-teams-rail { align-self: auto; contain: none; }
	.ip-teams-rail > .ip-card { position: static; height: auto; overflow: visible; }
}
@container (max-width: 759px) {
	.ip-log-rail { align-self: auto; contain: none; }
	.ip-log-rail > .ip-card { position: static; height: auto; overflow: visible; }
}
.ip-rail-sheet .ip-flyout__body > :is(.ip-teams-rail, .ip-log-rail) {
	align-self: auto;
	contain: none;
}
.ip-rail-sheet .ip-flyout__body > :is(.ip-teams-rail, .ip-log-rail) > .ip-card {
	position: static;
	height: auto;
	overflow: visible;
}

/* The pane is the scroller but the card inside it is the shape, so the bar
   runs the pane's full height beside a card whose corners curve away from it —
   and Edge's overlay bar was drawn over the card's edge, straight through both
   corners. The shell's own thin bar (::-webkit-scrollbar above) takes room
   rather than overlaying, so it stays beside the card; its track is held in
   from each end by the 4px of shadow room plus a card's corner, so the thumb
   only ever runs beside the straight part of the edge. The thumb keeps the
   shell's 5px but stands off the card — flush against it, it read as a
   thickened border rather than a bar. The gap is a transparent border all
   round rather than on the card's side alone: a one-sided border squares the
   thumb's corners on that side, since the inner radius is the outer less it.
   No scrollbar-width here: Chromium ignores every ::-webkit-scrollbar rule on
   an element that sets it, and that is what put the native bar back. */
:is(.ip-report-side, :is(.ip-teams-rail, .ip-log-rail) > .ip-card)::-webkit-scrollbar { width: 9px; }
:is(.ip-report-side, :is(.ip-teams-rail, .ip-log-rail) > .ip-card)::-webkit-scrollbar-track {
	margin-block: 14px;
}
:is(.ip-report-side, :is(.ip-teams-rail, .ip-log-rail) > .ip-card)::-webkit-scrollbar-thumb {
	border: 2px solid transparent;
	border-radius: 9px;
	background-clip: padding-box;
}
@supports not selector(::-webkit-scrollbar) {
	:is(.ip-teams-rail, .ip-log-rail) > .ip-card,
	.ip-report-side { scrollbar-width: thin; }
}
@container (max-width: 787px) {
	.ip-teams-rail,
	.ip-people-rail { position: static; max-height: none; overflow: visible; }
}
.ip-log-layout,
.ip-report-layout { container-type: inline-size; }
@container (max-width: 759px) {
	.ip-log-rail { position: static; max-height: none; overflow: visible; }
}
@container (max-width: 763px) {
	.ip-report-side { position: static; max-height: none; overflow: visible; }
}

/* Wrapped, a rail is not drawn under the list at all: below forty team cards it
   was a pane nobody scrolled to, describing a pick made a screen away.
   rail-sheet.js lifts it into the entry sheet when a card is picked or its
   button pressed, and reads the slot it leaves here to know the layout has
   wrapped — the breakpoints stay in these queries and nowhere else. Keyed to
   the slot so a rail the script never reached still shows, where it was.
   The report builder's side is left alone: it wraps above the document, as
   the controls you start from, not below it. */
@container (max-width: 787px) {
	.ip-rail-slot { --ip-rail-sheet: 1; }
	.ip-rail-slot + :is(.ip-teams-rail, .ip-people-rail) { display: none; }
}
@container (max-width: 759px) {
	.ip-rail-slot { --ip-rail-sheet: 1; }
	.ip-rail-slot + .ip-log-rail { display: none; }
}
/* In the sheet it is a block in the body, with none of the pane's pinning,
   scrolling or width — the sheet's body scrolls, and the sheet is the width. */
.ip-rail-sheet .ip-flyout__body > :is(.ip-teams-rail, .ip-people-rail, .ip-log-rail) {
	position: static;
	flex: none;
	max-width: none;
	max-height: none;
	overflow: visible;
	padding: 0;
	margin: 0;
}

.ip-team-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
/* A tile that selects into the rail, like .ip-evidence — so its edge, hover and
   selected ring are the evidence tile's, not a form's choice card's. */
.ip-team-card {
	text-align: left;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	padding: 16px;
	cursor: pointer;
	box-shadow: var(--ip-shadow-sm);
	display: flex;
	flex-direction: column;
	gap: 10px;
}
/* Only where something hovers: a tap leaves the pointer on the card it touched, and
   the card stayed outlined as if it were selected. */
@media (hover: hover) { .ip-team-card:hover { border-color: var(--ip-teal); } }
/* The ring is what tells the selected card from the one under the pointer. */
.ip-team-card.is-on {
	border-color: var(--ip-teal);
	box-shadow: 0 0 0 1px var(--ip-teal), var(--ip-shadow-sm);
}
.ip-team-card__name { font-size: var(--ip-font-base); font-weight: 600; letter-spacing: -0.01em; }
.ip-team-card__help { font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.5; }
.ip-team-card__stats {
	display: flex;
	gap: 14px;
	font-size: var(--ip-font-xs);
	color: var(--ip-faint);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	/* Down to the foot: a long name in the same row stretches every card in
	   it, and the figures should still line up across the row. */
	margin-top: auto;
	padding-top: 2px;
}

/* Map — a MapLibre globe (vendor/maplibre-gl.css), dressed in the tokens here
   rather than by editing its sheet. The canvas is its own stacking context:
   the library's controls sit on z-indexes of their own, which would otherwise
   draw over the sticky top bar as the page scrolls under it.

   The stage behind the globe is the card's own background — the map's canvas
   is transparent off the edge of the world. Light: a pale sky round a lit
   middle. Dark: space, with stars and the accent's glow behind the globe, the
   same glow the page itself is lit by (see "Background"). */
.ip-map { overflow: hidden; }
.ip-map__canvas {
	height: clamp(360px, 62dvh, 680px);
	isolation: isolate;
	z-index: 0;
	font-family: var(--ip-sans);
	background: radial-gradient(circle closest-side at 50% 50%, var(--ip-surface) 40%, var(--ip-map-space) 140%);
}
/* The page is the window: the column is exactly the room under the top bar,
   as the rail's card is, and the map takes whatever the toolbar leaves — so
   the page never scrolls, and a toolbar whose chips wrap to a second line
   costs the map that line rather than pushing it off the bottom. Narrower,
   the rail is a sheet (rail-sheet.js) rather than a column, so the same holds
   on a phone: the toolbar, then the map to the bottom of the screen. */
.ip-map-layout > .ip-teams-main {
	display: flex;
	flex-direction: column;
	height: calc(100dvh - var(--ip-stick-top) - 16px);
}
.ip-map-layout > .ip-teams-main > .ip-stack-sm { flex: 1; min-height: 0; }
.ip-map-layout .ip-map { flex: 1; min-height: 280px; }
.ip-map-layout .ip-map__canvas { height: 100%; }
/* With the rail a sheet, the toolbar is three short rows rather than four: the
   layer switch with the button that opens the places beside it, the search,
   then the chips on one line (sideways on a phone, as every toolbar's are).
   The count goes — the places sheet opens on the same figures — and so does
   the space it held, which on a phone was a whole line under the search. */
@container (max-width: 787px) {
	.ip-map-toolbar > .ip-search { order: 1; flex-basis: 100%; }
	.ip-map-toolbar > .ip-toolbar__filters { order: 2; }
	.ip-map-toolbar .ip-toolbar__meta { display: none; }
}
:root[data-theme="dark"] .ip-map__canvas {
	background:
		radial-gradient(circle closest-side at 50% 50%, rgba(var(--ip-glow), 0.26) 62%, rgba(var(--ip-glow), 0.08) 92%, transparent 135%),
		radial-gradient(1px 1px at 23px 41px, var(--ip-map-star), transparent),
		radial-gradient(1px 1px at 97px 13px, var(--ip-map-star), transparent),
		radial-gradient(1.5px 1.5px at 61px 129px, var(--ip-map-star), transparent),
		radial-gradient(1px 1px at 150px 77px, var(--ip-map-star), transparent),
		radial-gradient(1.5px 1.5px at 11px 190px, var(--ip-map-star), transparent),
		radial-gradient(1px 1px at 201px 233px, var(--ip-map-star), transparent),
		var(--ip-map-space);
	background-size: auto, 173px 173px, 211px 211px, 263px 263px, 307px 307px, 349px 349px, 389px 389px, auto;
}
.ip-map__canvas.maplibregl-map { font: var(--ip-font-xs) var(--ip-sans); }

/* A place: on Teams, FIRST's colour when every team there is in one program
   (.ip-prog--*, below); on Activities, the type's own hue, the one its tag
   wears on every screen. Plain ink when they are mixed — no one colour
   stands for "several" — rather than the label grey, which on the globe read
   as a dot that had not finished loading. */
.ip-map-dot--teal    { --prog: var(--ip-teal); }
.ip-map-dot--blue    { --prog: var(--ip-blue); }
.ip-map-dot--amber   { --prog: var(--ip-amber); }
.ip-map-dot--violet  { --prog: var(--ip-violet); }
.ip-map-dot--pink    { --prog: var(--ip-pink); }
.ip-map-dot--neutral { --prog: var(--ip-label); }
.ip-map-dot--mixed   { --prog: var(--ip-ink-soft); }
.ip-map-dot--teal, .ip-map-dot--blue, .ip-map-dot--amber, .ip-map-dot--violet,
.ip-map-dot--pink, .ip-map-dot--neutral, .ip-map-dot--mixed { --prog-fg: var(--ip-on-accent); }

/* The dot pointed at and the dot picked, drawn as elements over their WebGL
   circles (map.js says why), positioned by MapLibre, which owns their
   `position` and `transform` — so everything that moves is on the children.
   One slipping round the back of the world is faded out by the library and
   taken out of reach here. */
.ip-globe-dot {
	width: var(--dot, 12px);
	height: var(--dot, 12px);
	padding: 0;
	border: 0;
	background: none;
	cursor: pointer;
	transition: opacity 260ms ease, visibility 0s;
}
.ip-globe-dot.maplibregl-marker-covered {
	visibility: hidden;
	pointer-events: none;
	transition: opacity 260ms ease, visibility 0s linear 260ms;
}
.ip-globe-dot.is-hot,
.ip-globe-dot:hover,
.ip-globe-dot:focus-visible { z-index: 3; outline: none; }
.ip-globe-dot.is-on { z-index: 2; }
.ip-globe-dot__core {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	border-radius: 50%;
	background: var(--prog, var(--ip-teal));
	box-shadow:
		0 0 0 1.5px var(--ip-surface),
		0 0 16px 2px color-mix(in srgb, var(--prog, var(--ip-teal)) 60%, transparent);
	color: var(--prog-fg, var(--ip-on-accent));
	font-family: var(--ip-mono);
	font-size: var(--ip-font-xs);
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	letter-spacing: -0.02em;
	transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms ease;
}
/* .is-hot is put on a frame after the element is made, so it grows to this
   rather than appearing at it: a fresh element has nothing to transition from. */
.ip-globe-dot.is-hot .ip-globe-dot__core,
.ip-globe-dot:hover .ip-globe-dot__core { transform: scale(1.2); }
.ip-globe-dot:focus-visible .ip-globe-dot__core {
	box-shadow: 0 0 0 2px var(--ip-surface), 0 0 0 4px var(--ip-focus);
}
.ip-globe-dot.is-on .ip-globe-dot__core {
	transform: scale(1.15);
	box-shadow:
		0 0 0 2px var(--ip-surface),
		0 0 0 4px var(--ip-ink),
		0 0 22px 6px color-mix(in srgb, var(--prog, var(--ip-teal)) 70%, transparent);
}
/* A ring that leaves each dot and fades — a place where something happened,
   rather than a pin. Each starts at its own point in the beat (--beat). */
.ip-globe-dot__pulse {
	position: absolute;
	inset: 0;
	border: 1.5px solid var(--prog, var(--ip-teal));
	border-radius: 50%;
	opacity: 0;
	animation: ip-globe-pulse 3.2s cubic-bezier(0.2, 0.6, 0.3, 1) var(--beat, 0ms) infinite;
	pointer-events: none;
}
@keyframes ip-globe-pulse {
	0%        { transform: scale(1);   opacity: 0.85; }
	70%, 100% { transform: scale(2.9); opacity: 0; }
}
/* What is at a place, while a dot is pointed at (map.js, showCard()). Frosted
   over the globe. Only to look at — the pointer passes through it to the map. */
.ip-globe-card {
	position: absolute;
	z-index: 4;
	top: 0;
	left: 0;
	width: 300px;
	max-width: calc(100% - 16px);
	padding: 12px 14px 12px;
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	background: var(--ip-header-bg);
	-webkit-backdrop-filter: blur(18px) saturate(1.4);
	backdrop-filter: blur(18px) saturate(1.4);
	box-shadow: var(--ip-shadow-pop);
	color: var(--ip-ink);
	font-family: var(--ip-sans);
	pointer-events: none;
	opacity: 0;
	transform: translateY(6px) scale(0.98);
	transform-origin: 50% 100%;
	transition: opacity 160ms ease, transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ip-globe-card.is-below { transform-origin: 50% 0; transform: translateY(-6px) scale(0.98); }
.ip-globe-card.is-open { opacity: 1; transform: none; }
.ip-globe-card__title {
	font-size: var(--ip-font-sm);
	font-weight: 600;
	letter-spacing: -0.01em;
	overflow-wrap: anywhere;
}
.ip-globe-card__meta {
	margin-top: 2px;
	font-size: var(--ip-font-xs);
	color: var(--ip-muted);
	font-variant-numeric: tabular-nums;
}
.ip-globe-card__rows {
	display: flex;
	flex-direction: column;
	gap: 7px;
	margin-top: 10px;
	padding-top: 10px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-globe-card__row {
	display: flex;
	align-items: center;
	gap: 7px;
	min-width: 0;
	font-size: var(--ip-font-xs);
}
.ip-globe-card__row > .ip-prog,
.ip-globe-card__row > .ip-tag,
.ip-globe-card__row > .ip-team-avatar { flex: none; }
.ip-globe-card__name {
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-weight: 500;
}
.ip-globe-card__aside {
	flex: none;
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	color: var(--ip-faint);
}
.ip-globe-card__more {
	margin-top: 9px;
	font-size: var(--ip-font-xs);
	color: var(--ip-muted);
}
@media (prefers-reduced-motion: reduce) {
	.ip-globe-dot__pulse { animation: none; }
	.ip-globe-dot__core, .ip-globe-card { transition: none; }
}

/* The controls: zoom, the compass (which also undoes a tilt), and "show
   every place". MapLibre's glyphs are dark pictures, so the dark theme turns
   them over rather than redrawing them. */
.ip-map .maplibregl-ctrl-group {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	overflow: hidden;
}
.ip-map .maplibregl-ctrl-group:not(:empty) { box-shadow: var(--ip-shadow-sm); }
.ip-map .maplibregl-ctrl-group button { width: 32px; height: 32px; background-color: transparent; }
.ip-map .maplibregl-ctrl-group button + button { border-top: 1px solid var(--ip-border-soft); }
.ip-map .maplibregl-ctrl-group button:not(:disabled):hover { background-color: var(--ip-surface-sunk); }
.ip-map .maplibregl-ctrl-group button:focus-visible { box-shadow: inset 0 0 0 2px var(--ip-focus); }
:root[data-theme="dark"] .ip-map .maplibregl-ctrl-icon,
:root[data-theme="dark"] .ip-map .maplibregl-ctrl-attrib-button { filter: invert(1); }
.ip-map-whole { display: grid; place-items: center; color: var(--ip-ink-soft); font-size: var(--ip-font-base); }
.ip-map .maplibregl-ctrl-attrib {
	background: var(--ip-header-bg);
	color: var(--ip-muted);
	font-size: var(--ip-font-xs);
}
/* MapLibre's sheet paints the folded box black on white, and loads after
   this one — so the colour is restated here, where it outranks it. */
.ip-map .maplibregl-ctrl-attrib.maplibregl-compact { background: var(--ip-surface); color: var(--ip-muted); }
.ip-map .maplibregl-ctrl-attrib a { color: var(--ip-teal); }

/* The rail beside it: the places, then what is at one. Rows rather than an
   .ip-table — two or three lines each, in a column 330px wide. */
.ip-map-list { display: flex; flex-direction: column; gap: 2px; }
.ip-map-place,
.ip-map-item {
	display: flex;
	gap: 10px;
	width: 100%;
	padding: 8px;
	border: 0;
	border-radius: 8px;
	background: none;
	color: inherit;
	font: inherit;
	text-align: left;
	text-decoration: none;
	cursor: pointer;
}
.ip-map-place { align-items: center; font-size: var(--ip-font-sm); }
.ip-map-item { flex-direction: column; gap: 3px; }
.ip-map-place:hover,
.ip-map-place:focus-visible,
.ip-map-item:hover,
.ip-map-item:focus-visible { background: var(--ip-surface-sunk); color: inherit; }
.ip-map-place__dot {
	flex: none;
	width: 10px;
	height: 10px;
	border-radius: 50%;
	background: var(--prog, var(--ip-teal));
}
.ip-map-place__label { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.ip-map-place__count {
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	color: var(--ip-muted);
}
.ip-map-item__head { display: flex; align-items: center; gap: 7px; min-width: 0; }
.ip-map-item__meta { font-size: var(--ip-font-xs); color: var(--ip-faint); }
.ip-map-item__name { font-size: var(--ip-font-sm); font-weight: 600; overflow-wrap: anywhere; }
.ip-map-item__note { font-size: var(--ip-font-xs); color: var(--ip-muted); overflow-wrap: anywhere; }
/* Activities left off the map, counted on a line rather than listed. */
.ip-map-unplaced { font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.55; padding: 0 8px; }

/* Programs — the one place a program is matched to its colour. Everything that
   shows a program (the .ip-prog tag, the add-team picker, a map dot) reads
   --prog and --prog-fg (the solid mark) or --prog-bg and --prog-ink (the tag)
   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); --prog-bg: var(--ip-frc-bg); --prog-ink: var(--ip-frc-ink); }
.ip-prog--FTC, .ip-picker-btn.is-FTC { --prog: var(--ip-ftc); --prog-fg: var(--ip-ftc-fg); --prog-bg: var(--ip-ftc-bg); --prog-ink: var(--ip-ftc-ink); }
.ip-prog--FLL, .ip-picker-btn.is-FLL { --prog: var(--ip-fll); --prog-fg: var(--ip-fll-fg); --prog-bg: var(--ip-fll-bg); --prog-ink: var(--ip-fll-ink); }
.ip-prog--FGC, .ip-picker-btn.is-FGC { --prog: var(--ip-fgc); --prog-fg: var(--ip-fgc-fg); --prog-bg: var(--ip-fgc-bg); --prog-ink: var(--ip-fgc-ink); }

/* Drawn as an .ip-tag — its shape is that rule's — in the program's tint and
   hue, so a program reads as one of the site's tags. It was FIRST's colour
   solid under dark text, which beside the tinted tags looked like a badge from
   some other kit, and a column of solid yellow FLLs out-shouted every row. Mono
   because it often carries a team number. */
.ip-prog {
	display: inline-block;
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	/* One line, cut rather than spilling: a team named "The Gearheads of Greater
	   Western Sydney Grammar School" ran past the edge of the sheet it sat in.
	   But never shrunk by its row: in a flex row overflow:hidden lets it give way
	   first, so "FTC" became "F…" beside a long team name. max-width still cuts
	   the long case. */
	flex-shrink: 0;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	vertical-align: bottom;
	background: var(--prog-bg, var(--ip-rule));
	color: var(--prog-ink, var(--ip-muted));
}

/* A row's teams in a table cell: one team as its tag and number, the way the
   Teams helped cards show it; several as a tag per program and how many
   (outreach/list.js, teamCell). One line, cut rather than wrapped. */
.ip-team-cell {
	display: flex;
	align-items: center;
	gap: 5px;
	min-width: 0;
	overflow: hidden;
}
.ip-team-cell__figure {
	flex-shrink: 0;
	margin-left: 4px;
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	color: var(--ip-faint);
	white-space: nowrap;
}
/* Ahead of the tags, wide enough for "999 teams", so the tags start at one
   place down the column whatever the count. */
.ip-team-cell__figure--count {
	order: -1;
	margin: 0 6px 0 0;
	min-width: 7.5ch;
	color: var(--ip-ink-soft);
}

/* -----------------------------------------------------------------------------
   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, the team cards and the
   rail. White behind it for the same reason the org logo gets one: these are
   logos drawn for paper. An explicit box because it renders from a data: URI,
   and a broken image must not collapse the row it sits in. `pixelated` because
   the originals are 40x40: smoothing a tiny logo upward turns it into a smear.
   One rule, with --lg after it: there used to be two, and the later one's size
   beat --lg, so the rail's "large" avatar was drawn at 30px. */
.ip-team-avatar {
	flex: 0 0 auto;
	width: 30px;
	height: 30px;
	object-fit: contain;
	image-rendering: pixelated;
	border-radius: 7px;
	background: var(--ip-logo-ground);
	border: 1px solid var(--ip-border-soft);
	padding: 2px;
}
.ip-team-avatar--lg { width: 40px; height: 40px; border-radius: 8px; }
/* The team switcher's size: a line of its own text, beside the program tag, so
   the 30px row size would dwarf it. The frame shrinks with the picture. */
.ip-team-avatar--sm { width: 20px; height: 20px; border-radius: 5px; padding: 1px; }
/* A team's contact addresses in the detail rail, the primary first; each on a
   line of its own so a long one wraps rather than pushing the rail wide. */
.ip-team-emails {
	display: flex;
	flex-direction: column;
	gap: 2px;
	margin-top: 4px;
	font-size: var(--ip-font-xs);
	overflow-wrap: anywhere;
}

.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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.ip-mini-stat__label { font-size: var(--ip-font-xs); 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;
	/* The wall is every photo in the window, and a tile below the fold is still
	   a shimmer waiting on a lazy image — one restyle and repaint a frame each,
	   for something nobody can see. Skipping off-screen tiles keeps scrolling as
	   cheap with three hundred tiles as with six. The height is the first guess
	   before a tile is drawn; `auto` keeps the real one after, so the scrollbar
	   settles rather than jumping. The tile's own shadow and selection ring sit
	   outside what this clips. */
	content-visibility: auto;
	contain-intrinsic-size: auto 300px;
}
@media (hover: hover) { .ip-evidence:hover { border-color: var(--ip-teal); } }
.ip-evidence:hover { 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; }
/* The tile's shimmer stays until the picture's bytes land — the skeleton page
   draws the tile, but without this the frame went blank between the data
   arriving and the image arriving. Same hand-over as `.ip-file-thumb`. */
.ip-evidence__preview { position: relative; }
.ip-evidence__preview img { opacity: 0; transition: opacity 0.2s ease; }
.ip-evidence__preview.is-ready img { opacity: 1; }
.ip-evidence__preview .skeleton { position: absolute; inset: 0; display: block; }
.ip-evidence__kind {
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	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: var(--ip-font-sm); font-weight: 600; line-height: 1.35; }
.ip-evidence__from { font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.5; flex: 1; }
.ip-evidence__link {
	font-size: var(--ip-font-xs);
	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); }

/* One program's teams and links. FRC and FTC share no criteria, so with both
   switched on each gets its own section, ruled off from the one before. */
.ip-link-group + .ip-link-group {
	border-top: 1px solid var(--ip-border-soft);
	margin-top: 16px;
	padding-top: 14px;
}

/* 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: var(--ip-font-xs); font-weight: 600; }
.ip-link-row__text { font-size: var(--ip-font-sm); color: var(--ip-label); line-height: 1.45; text-wrap: pretty; }
.ip-link-row__tag {
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--ip-font-xs);
	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: var(--ip-font-xs);
	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);
}
/* Linked for some of the program's selected teams but not all of them. */
.ip-link-pick.is-partial { border-color: var(--ip-teal); }
.ip-link-pick.is-partial .ip-link-pick__mark { border-color: var(--ip-teal); color: var(--ip-teal); }
.ip-link-pick__text { flex: 1; min-width: 0; font-size: var(--ip-font-sm); 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-variant-numeric: tabular-nums; font-size: var(--ip-font-xs); color: var(--ip-teal); margin-top: 1px; }
.ip-output__name { font-size: var(--ip-font-sm); font-weight: 600; }
.ip-output__hint { font-size: var(--ip-font-xs); 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__label { font-size: var(--ip-font-sm); flex: 1; }
.ip-section-toggle.is-off .ip-section-toggle__label { color: var(--ip-faint); }
.ip-section-toggle__words { font-size: var(--ip-font-xs); color: var(--ip-faint); font-family: var(--ip-mono); font-variant-numeric: tabular-nums; }

.ip-doc { padding: 34px 40px 48px; max-width: 780px; }
.ip-doc__title {
	font-size: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.025em;
	margin-top: 10px;
	line-height: 1.15;
	text-wrap: pretty;
}
.ip-doc__meta { font-size: var(--ip-font-sm); 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: var(--ip-font-h1); font-weight: 600; letter-spacing: -0.025em; }
.ip-doc__stat-label { font-size: var(--ip-font-xs); 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: var(--ip-font-base); font-weight: 600; letter-spacing: -0.01em; }
.ip-doc__text {
	font-size: var(--ip-font-sm);
	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: var(--ip-font-sm); font-style: italic; color: var(--ip-ink-soft); line-height: 1.6; }
.ip-doc__cite-from { font-size: var(--ip-font-xs); 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 organisation 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: var(--ip-font-h1);
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1.15;
	/* The placeholder is a bar shorter than the plan name beside its tag. */
	min-height: 1.15em;
}
.ip-billing-overview__line {
	margin-top: 6px;
	max-width: 68ch;
	font-size: var(--ip-font-sm);
	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: var(--ip-font-xs); color: var(--ip-muted); }
.ip-billing-fact__value { font-size: var(--ip-font-base); font-weight: 600; letter-spacing: -0.01em; }
.ip-billing-fact__note { font-size: var(--ip-font-xs); color: var(--ip-muted); }
.ip-billing-fact .ip-btn--link { margin-top: 4px; font-size: var(--ip-font-xs); }

.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: var(--ip-font-xs);
	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: var(--ip-font-base); font-weight: 600; }
.ip-billing-bar__meta { margin-top: 2px; font-size: var(--ip-font-xs); 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: var(--ip-font-sm); font-weight: 600; }
.ip-tier__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.ip-tier__amount { font-size: var(--ip-font-h1); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; }
.ip-tier__per { font-size: var(--ip-font-sm); color: var(--ip-muted); }
.ip-tier__sub { margin-top: 4px; font-size: var(--ip-font-xs); color: var(--ip-muted); }
.ip-tier__blurb { font-size: var(--ip-font-sm); 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: var(--ip-font-sm); 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: var(--ip-font-sm);
	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: var(--ip-font-sm);
	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: var(--ip-font-xs);
	color: var(--ip-muted);
}

.ip-field__error { font-size: var(--ip-font-xs); 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: var(--ip-font-sm); font-weight: 600; }
.ip-billing-note__body { margin-top: 6px; font-size: var(--ip-font-xs); line-height: 1.55; color: var(--ip-muted); }
.ip-billing-foot { font-size: var(--ip-font-xs); 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;
}

/* The search and the menu under it. Sticky, so the way to the next section —
   and which one you are in — does not scroll away with the content. It rests
   where everything else that sticks does: at 12px it slid up under the top bar. */
.ip-settings__menu {
	position: sticky;
	top: var(--ip-stick-top);
	display: flex;
	flex-direction: column;
	gap: 16px;
}
/* The whole width of the menu: in a toolbar the box shares a row and takes a
   flex share of it, here it has the row to itself. */
.ip-settings__search .ip-search { display: block; width: 100%; min-width: 0; }
.ip-settings__nav {
	display: flex;
	flex-direction: column;
	gap: 18px;
}

/* A setting found inside a section by the search, listed under its section's
   entry and indented to the entry's label, past its icon. */
.ip-settings__hit {
	display: block;
	padding: 5px 10px 5px 35px;
	border-radius: 8px;
	font-size: var(--ip-font-sm);
	color: var(--ip-muted);
	text-decoration: none;
}
.ip-settings__hit:hover { color: var(--ip-ink); }
/* The result the arrow keys are on — the hover look, since it is the same
   "this one" without a pointer. */
.ip-settings__link.is-cursor,
.ip-settings__hit.is-cursor { background: var(--ip-veil-hover); color: var(--ip-ink); }
.ip-settings__none { padding: 2px 10px; font-size: var(--ip-font-sm); color: var(--ip-faint); }

/* The setting a search result led to, picked out for a moment so the eye
   finds it in a section of several. An inset shadow rather than a background:
   it paints over whatever the element's own fill is and leaves it as it was. */
.ip-settings__found { animation: ip-settings-found 1.8s ease-out; }
@keyframes ip-settings-found {
	from { box-shadow: inset 0 0 0 100vmax var(--ip-mint); }
	to   { box-shadow: inset 0 0 0 100vmax transparent; }
}
.ip-settings__group { display: flex; flex-direction: column; gap: 2px; }
.ip-settings__group-label {
	padding: 0 10px 6px;
	font-size: var(--ip-font-xs);
	font-weight: 600;
	color: var(--ip-faint);
}
.ip-settings__link {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 7px 10px;
	border-radius: 8px;
	font-size: var(--ip-font-base);
	color: var(--ip-nav-fg);
	text-decoration: none;
}
.ip-settings__link:hover { color: var(--ip-ink); }
.ip-settings__link .bi { font-size: 1.05em; 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); }

/* -----------------------------------------------------------------------------
   Plan gate — a feature the plan leaves out (PlanGate.php, planLock())
   -------------------------------------------------------------------------- */

/* After the menus on purpose: their icon rules match the lock's glyph too,
   and would turn it grey, or teal on the page you are on. */
/* The upgrade sparkle, in the tag's own text size rather than a pixel value, and
   centred on its box: Bootstrap Icons otherwise lower the glyph to a text
   baseline, which in a pill reads as off-centre. */
.ip-plan-lock > .bi { display: inline-flex; align-items: center; justify-content: center; line-height: 1; font-size: 1em; color: inherit; margin: 0; }
.ip-plan-lock > .bi::before { vertical-align: 0; }

/* The icon-only form, at the end of a menu link. Round, so it reads as a mark
   on the link rather than a second, smaller label. */
.ip-plan-lock--icon {
	justify-content: center;
	width: 20px;
	height: 20px;
	padding: 0;
	margin-left: auto;
	flex: 0 0 20px;
}

/* The page under the cover: its own markup and skeletons, blurred and faded
   back, so it reads as the feature being there but out of reach. Tall enough
   to hold the card whatever the page's own height. */
.ip-gate { position: relative; min-height: 460px; }
.ip-gate__behind {
	filter: blur(5px);
	opacity: 0.55;
	pointer-events: none;
	user-select: none;
}
.ip-gate__cover {
	position: absolute;
	inset: 0;
	display: flex;
	justify-content: center;
	align-items: flex-start;
	padding: clamp(40px, 12vh, 120px) 16px 16px;
}
.ip-gate__card {
	width: 100%;
	max-width: 400px;
	padding: 28px 28px 24px;
	text-align: center;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 10px;
}
.ip-gate__icon {
	width: 44px;
	height: 44px;
	border-radius: 999px;
	display: grid;
	place-items: center;
	background: var(--ip-violet-bg);
	color: var(--ip-violet);
	font-size: var(--ip-font-h3);
	margin-bottom: 4px;
}
/* Icons in fixed-size boxes — checks, close buttons, thumbnails, toggles, step
   dots. Bootstrap Icons lower every glyph by .125em to sit on a text baseline,
   and inherit the page's 1.5 line-height; in a box that is centred by flex or
   grid, both push the glyph off the middle. One rule for all of them rather than
   a fix per component, so the next box gets it by adding itself here. */
.ip-award__mark > .bi,
.ip-check > .bi,
.ip-flyout__close > .bi,
.ip-link-pick__mark > .bi,
.ip-section__mark > .bi,
.ip-setting__thumb > .bi,
.ip-theme-toggle > .bi,
.ip-notif__toggle > .bi,
.ip-type__glyph > .bi,
.ip-file-thumb > .bi,
.fm-tile-mark > .bi,
.fm-tile-toggle > .bi,
.gal-icon-btn > .bi,
.ip-avatar > .bi,
.ip-progress__dot > .bi,
.ip-step__dot > .bi,
.ip-section-toggle > .bi,
.ip-section__toggle > .bi,
.app-topbar__menu > .bi,
.sidebar-foot-btn > .bi,
.ip-seg__btn > .bi {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	line-height: 1;
}
.ip-award__mark > .bi::before,
.ip-check > .bi::before,
.ip-flyout__close > .bi::before,
.ip-link-pick__mark > .bi::before,
.ip-section__mark > .bi::before,
.ip-setting__thumb > .bi::before,
.ip-theme-toggle > .bi::before,
.ip-notif__toggle > .bi::before,
.ip-type__glyph > .bi::before,
.ip-file-thumb > .bi::before,
.fm-tile-mark > .bi::before,
.fm-tile-toggle > .bi::before,
.gal-icon-btn > .bi::before,
.ip-avatar > .bi::before,
.ip-progress__dot > .bi::before,
.ip-step__dot > .bi::before,
.ip-section-toggle > .bi::before,
.ip-section__toggle > .bi::before,
.app-topbar__menu > .bi::before,
.sidebar-foot-btn > .bi::before,
.ip-seg__btn > .bi::before {
	vertical-align: 0;
}

.bi[hidden] { display: none !important; }

/* Bootstrap Icons lower their glyph by .125em to sit on a text baseline, which in
   a circle just reads as off-centre. Remove the shift and the line-height slack. */
.ip-gate__icon > .bi { display: flex; line-height: 1; }
.ip-gate__icon > .bi::before { vertical-align: 0; }
.ip-gate__title { margin: 0; font-size: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
.ip-gate__body { margin: 0 0 6px; font-size: var(--ip-font-sm); line-height: 1.5; color: var(--ip-muted); text-wrap: pretty; }

/* Every section on one page (templates/settings/page.php). The gap between
   sections is several times the gap inside one, so where one setting ends and
   the next begins reads from the spacing alone — a rule between them would be
   one more line on a page that is mostly lines. */
.ip-settings__body { display: flex; flex-direction: column; gap: 56px; min-width: 0; }
.ip-settings__section {
	display: flex;
	flex-direction: column;
	gap: 16px;
	/* Where a jump from the menu puts the heading: clear of the top bar, at the
	   line everything sticky rests on. settings.js reads it back for the spy. */
	scroll-margin-top: var(--ip-stick-top);
}
/* Every section runs the full width of the column, like the rest of the app.
   No section sets a width of its own: five once capped themselves at 940px and
   three did not, so they stopped at different edges down the one page. */
.ip-settings__head { display: flex; flex-direction: column; gap: 4px; }
.ip-settings__title { margin: 0; font-size: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
.ip-settings__summary { font-size: var(--ip-font-sm); line-height: 1.5; color: var(--ip-muted); text-wrap: pretty; }

/* A setting, one per row: what it is on the left (with a line of explanation
   when it needs one), what it is set to on the right, and the button that
   changes it at the far edge — so a card of them reads down two straight
   columns rather than as a form. Worn inside an .ip-card--flush, under an
   .ip-card__head. The value is right-aligned against its button so the eye
   goes from the setting's name straight across to what to do about it. */
.ip-setting {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px 16px;
	padding: 14px 18px;
	min-height: 64px;
}
.ip-setting + .ip-setting { border-top: 1px solid var(--ip-border-soft); }
.ip-setting__main { flex: 1 1 180px; min-width: 0; }
.ip-setting__label { font-size: var(--ip-font-sm); font-weight: 600; color: var(--ip-ink); }
.ip-setting__hint {
	font-size: var(--ip-font-xs);
	line-height: 1.5;
	color: var(--ip-muted);
	margin-top: 3px;
	max-width: 60ch;
	text-wrap: pretty;
}
.ip-setting__value {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	flex-wrap: wrap;
	gap: 6px;
	min-width: 0;
	max-width: 100%;
	font-size: var(--ip-font-sm);
	text-align: right;
	overflow-wrap: anywhere;
}
.ip-setting__value code { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; font-size: var(--ip-font-sm); color: inherit; }
.ip-setting__actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
/* Settings > Security: rows grouped under a small heading, each method led by an
   icon tile, and a method's own items (the passkeys) inset in a sunken panel
   aligned under its text so they read as its children, not as more methods. */
.ip-setting-group {
	padding: 10px 18px 6px;
	font-size: var(--ip-font-xs);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-faint);
	background: var(--ip-surface-sunk);
	border-top: 1px solid var(--ip-border-soft);
}
.ip-setting-group:first-child { border-top: 0; }
.ip-setting__icon {
	flex: 0 0 auto;
	width: 36px;
	height: 36px;
	display: grid;
	place-items: center;
	border-radius: 10px;
	background: var(--ip-surface-sunk);
	border: 1px solid var(--ip-border-soft);
	color: var(--ip-muted);
	font-size: 1.05rem;
}
.ip-setting-sub {
	margin: 0 18px 14px 70px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	background: var(--ip-surface-sunk);
}
.ip-setting-sub .ip-setting { padding: 10px 12px; min-height: 0; }
.ip-setting-sub .ip-setting__icon { width: 28px; height: 28px; border-radius: 8px; font-size: 0.9rem; background: transparent; }
.ip-setting-sub .ip-setting__label { font-weight: 500; }
.ip-setting + .ip-setting-sub { border-top: 1px solid var(--ip-border-soft); margin-top: -2px; }
.ip-setting:has(+ .ip-setting-sub) { border-bottom: 0; }
.ip-setting-sub + .ip-setting, .ip-setting-group + .ip-setting { border-top: 1px solid var(--ip-border-soft); }
/* Settings > Security. The authenticator's QR code on its light plate (dark
   modules whatever the theme, as on the pit display), and the recovery codes
   laid out to be copied or read off paper. */
.ip-qr-plate { width: 176px; height: 176px; padding: 8px; border-radius: 12px; background: var(--ip-logo-ground); margin: 0 auto; }
.ip-qr-plate svg { width: 100%; height: 100%; display: block; fill: var(--ip-code-ink); }
.ip-recovery-codes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; margin: 0; padding: 14px 16px; list-style: none; border: 1px solid var(--ip-border-soft); border-radius: 10px; background: var(--ip-surface-sunk); }
.ip-recovery-codes li { font-family: var(--ip-mono); font-variant-numeric: tabular-nums; font-size: var(--ip-font-sm); text-align: center; user-select: all; }
.ip-recovery-codes li.is-spent { text-decoration: line-through; color: var(--ip-muted); }
/* A value of more than one line — the live domain, and the one being
   connected under it — each line right-aligned on its own. */
.ip-setting__line {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	flex-wrap: wrap;
	gap: 6px;
	width: 100%;
}
/* An image as the value — the organisation logo — kept small enough that its
   row is no taller than the rest. Still clickable, like the button beside it. */
.ip-setting__thumb {
	width: 44px;
	height: 44px;
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	background: var(--ip-surface-sunk);
	display: grid;
	place-items: center;
	overflow: hidden;
	cursor: pointer;
	color: var(--ip-faint);
}
.ip-setting__thumb img { width: 100%; height: 100%; object-fit: contain; background: var(--ip-logo-ground); }

/* 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;
		/* The height of the search and tab row above the page, which a
		   section's jump has to clear as well as the bar. settings.js keeps it
		   measured; this is the shape before it has. */
		--ip-settings-tabs: 100px;
	}
	.ip-settings__menu {
		/* Stuck straight under the bar rather than at the 16px gap below it the
		   rest of the app rests at: the block is the page's own heading strip
		   while it scrolls, and a gap would show the sections passing through
		   it. */
		top: var(--ip-topbar-h, 62px);
		z-index: 2;
		gap: 8px;
		padding-top: 8px;
		border-bottom: 1px solid var(--ip-border-soft);
		background-color: var(--ip-canvas);
	}
	.ip-settings__nav {
		/* Positioned, which the plan lock's hidden label needs — with no
		   positioned ancestor inside this scroller it measured from the page
		   instead, and scrolled 640px sideways into nothing. */
		position: relative;
		flex-direction: row;
		gap: 2px;
		overflow-x: auto;
		padding-bottom: 6px;
		scrollbar-width: thin;
	}
	/* A found setting is one more pill in the row, beside its section's tab. */
	.ip-settings__hit { padding: 7px 10px; white-space: nowrap; }
	.ip-settings__none { padding: 7px 10px; white-space: nowrap; }
	/* The page's own grid behind the block in the dark theme, pinned to the
	   viewport as the page's is, so it reads as page and not as a patch. */
	:root[data-theme="dark"] .ip-settings__menu {
		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;
	}
	.ip-settings__group { flex-direction: row; gap: 2px; }
	.ip-settings__group-label { display: none; }
	.ip-settings__link { white-space: nowrap; }
	.ip-settings__section { scroll-margin-top: calc(var(--ip-topbar-h, 62px) + var(--ip-settings-tabs) + 16px); }
}

/* -----------------------------------------------------------------------------
   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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
.fc .fc-button {
	border-radius: 8px;
	font-size: var(--ip-font-sm);
	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: var(--ip-font-xs);
	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: var(--ip-font-xs); 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: var(--ip-font-xs);
	font-weight: 500;
	padding: 2px 6px;
	cursor: pointer;
}
/* A name too long for the day wraps to a second line and then ends in an
   ellipsis. Left alone FullCalendar clips it mid-letter at a single line, and
   at a laptop's width a day is ~95px wide: "Mentor introduction fror". */
.fc .fc-daygrid-event { white-space: normal; }
.fc .fc-daygrid-event .fc-event-title {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
	overflow-wrap: anywhere;
	line-height: 1.3;
}
.fc .fc-daygrid-more-link { font-size: var(--ip-font-xs); color: var(--ip-teal); }

/* On a phone the title takes a row of its own and the controls sit under it:
   the arrows, Today, Month and List side by side with a month's name are wider
   than the card, and List was pushed past its edge. */
@media (max-width: 640px) {
	.fc .fc-header-toolbar { flex-wrap: wrap; gap: 10px 8px; }
	.fc .fc-header-toolbar > .fc-toolbar-chunk:nth-child(2) { order: -1; flex: 1 0 100%; }
	.fc .fc-toolbar-title { font-size: var(--ip-font-h3); }
}

.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: var(--ip-font-sm); color: var(--ip-muted); padding: 10px 0; }
.fm-subform { padding: 14px; }
.fm-subform__title { font-size: var(--ip-font-sm); 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);
}

/* The preset colours a swatch drops (swatch-picker.js) — the suggestion
   panel's surface, border and shadow, holding one row of chips. Fixed and on
   <body> for the same reason that one is. */
.ip-swatch-pop {
	position: fixed;
	z-index: 1060;
	display: flex;
	gap: 8px;
	padding: 8px;
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	background: var(--ip-surface);
	box-shadow: var(--ip-shadow-pop);
}

/* One preset colour — in the drop-down above and in the Add dialog alike.
   Filled with the chip's ink, not its ground: in the dark theme every ground is
   a near-black tint, and six of those side by side are six squares of nothing.
   The chosen one wears a ring outside it, which is why its neighbours keep 8px
   away. */
.ip-swatch {
	width: 26px;
	height: 26px;
	flex: 0 0 26px;
	border: 0;
	border-radius: 7px;
	background: var(--swatch);
	padding: 0;
	cursor: pointer;
}
.ip-swatch:hover { box-shadow: 0 0 0 2px var(--ip-surface), 0 0 0 3px var(--ip-border); }
.ip-swatch.is-chosen { box-shadow: 0 0 0 2px var(--ip-surface), 0 0 0 4px var(--swatch); }

/* 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: var(--ip-font-xs);
	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-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)); 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: var(--ip-font-xs);
	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.
 *
 * Hidden is not stopped, though: a CSS animation keeps ticking under
 * `visibility:hidden`, and the shimmer slides a background, which is restyled
 * and repainted on the main thread every frame. Forty finished thumbnails were
 * forty of those, and the evidence wall stuttered under them. So the keyframes
 * end with the hand-over; the sheen freezes for the 0.2s fade, which nobody
 * sees under a picture fading in. */
.gal-thumb .skeleton,
.fm-tile .skeleton,
.ip-file-thumb .skeleton,
.ip-evidence__preview .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,
.ip-evidence__preview.is-ready .skeleton,
.ip-evidence__preview.is-failed .skeleton {
	opacity:0;
	visibility:hidden;
	animation:none;
}

.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;}
/* A team's logo frame — the size and corner of .ip-team-avatar and its --lg, so
   the real badge lands exactly where the placeholder was. */
.skeleton-logo {width:30px; height:30px; min-width:30px; border-radius:7px; flex:0 0 auto;}
.skeleton-logo--lg {width:40px; height:40px; min-width:40px; border-radius:8px;}
/* The Add a team dialog's logo, top right of its head. Absolute so the title and
   sub-line do not move when it appears; the head keeps clear of it either way. */
.ip-modal__head--logo { position: relative; padding-right: 72px; }
.ip-team-logo-slot { position: absolute; top: 16px; right: 20px; width: 40px; height: 40px; }
.ip-team-logo-slot[hidden] { display: none; }
/* 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; }

/* Plans & billing: the real overview sits in a flush card and pads itself, and a
   real plan is a 24px-padded tier, so the placeholders take the same insets —
   otherwise the bars touch the card's edge and the page grows when data lands. */
#billing-overview > .sk-lines { padding: 20px 22px; gap: 12px; }
/* Security: the rows are .ip-setting (22px sides), so the placeholders sit on the same inset. */
#two-factor-rows > .sk-lines { padding: 20px 22px; gap: 12px; }
.ip-tiers > .sk-card { padding: 24px; min-height: 230px; border-radius: 14px; }
.ip-tiers > .sk-card .sk-gap { margin-top: 18px; }
.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; }
/* The chip's own box — padding, border, type size and line — around a blank
   line, rather than a height typed in: a fixed number was shorter than the
   chip it stands in for, and the row grew when the real ones arrived. */
.skeleton.sk-chip   { display: inline-block; height: auto; box-sizing: border-box; padding: 6px 12px; border: 1px solid transparent; font-size: var(--ip-font-xs); font-weight: 500; 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-podium__card {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 10px;
	padding: 20px 16px;
	min-height: 196px;
}
.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);}
/* 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: var(--ip-font-sm);
	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: var(--ip-font-xs);
	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: var(--ip-font-sm); font-weight:600;}
.fm-tile-more-icon {font-size:var(--ip-font-h3);}

/* 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: var(--ip-font-xs);
	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:var(--ip-font-base);}
.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: var(--ip-font-sm);
}
.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, .gal-download {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: var(--ip-font-h2);}
.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);}
@media (prefers-reduced-motion: reduce) {
	.gal-layer.gal-from-right, .gal-layer.gal-from-left {transform:none;}
}
.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: var(--ip-font-xs);
	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);
}
/* … */

/* -----------------------------------------------------------------------------
   Touch

   A finger is not a cursor. Where the pointer is coarse, a control is never
   smaller than 44px in the direction it is tapped — the size a fingertip can
   hit without aiming — and fields are set at 16px, because iOS zooms the whole
   page into any field whose text is smaller and leaves it zoomed after.
   Desktop keeps the denser sizes; this is a property of the pointer, not of
   the screen's width.
   -------------------------------------------------------------------------- */

@media (pointer: coarse) {
	:is(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea) {
		font-size: var(--ip-font-base);
	}

	.ip-btn,
	.ip-chip,
	.ip-settings__link,
	.ip-lists__tab { min-height: 44px; }
	.ip-chip { display: inline-flex; align-items: center; }
	.skeleton.sk-chip { min-height: 44px; }

	/* The top bar's controls read this, so one line raises them all together. */
	:root { --ip-control-h: 44px; }

	.ip-flyout__close,
	.ip-btn--icon,
	.app-topbar__menu { width: 44px; height: 44px; flex-basis: 44px; }

	.ip-attend__cell, .ip-attend__add { height: 44px; }
	.ip-sessions__row { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 0.8fr) 44px; }
	.gal-close, .gal-remove, .gal-download { width: 44px; height: 44px; }
	.ip-swatch { width: 36px; height: 36px; flex-basis: 36px; }

	/* Too small to grow without unsettling the row they sit in, so the area
	   that answers a tap grows instead of the mark. */
	/* The tile's toggle is already absolutely placed, which anchors its own. */
	.ip-check,
	.ip-grip,
	.ip-lists__remove { position: relative; }
	.ip-check::after,
	.fm-tile-toggle::after,
	.ip-grip::after,
	.ip-lists__remove::after {
		content: '';
		position: absolute;
		inset: 50% auto auto 50%;
		width: 44px;
		height: 44px;
		transform: translate(-50%, -50%);
	}

	/* Segmented buttons and switches sit in a control that is already about 44px
	   tall, but the button inside is 33–36 — and the switch is 26. The area
	   grows up and down only, so neighbours in the row keep their own. */
	.ip-seg__btn::after,
	.ip-switch::before {
		content: '';
		position: absolute;
		inset: -6px 0;
	}

	/* Marks written inline by a page (the award ticks and "+ Plan"), too small to
	   grow without moving what is round them: the tap area grows, not the mark. */
	.ip-tap { position: relative; }
	.ip-tap::after {
		content: '';
		position: absolute;
		inset: -10px -8px;
	}
}

/* -----------------------------------------------------------------------------
   Release notes — the What's new page, and the pop-up after an update

   One renderer draws both (ReleaseNote.php). On the page each release is its
   own card; in the dialog they run on as one list, divided by a rule.
   -------------------------------------------------------------------------- */

.ip-release-list { display: flex; flex-direction: column; gap: 14px; }
.ip-release-list > .ip-card { padding: 20px 22px; }
.ip-release-list--dialog { gap: 0; }
.ip-release-list--dialog .ip-release + .ip-release {
	margin-top: 18px;
	padding-top: 18px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-release__head { margin-bottom: 12px; }
.ip-release__date {
	font-size: var(--ip-font-xs);
	font-family: var(--ip-mono);
	font-variant-numeric: tabular-nums;
	color: var(--ip-muted);
}
.ip-release__title {
	margin: 2px 0 0;
	font-size: var(--ip-font-h3);
	font-weight: 600;
	letter-spacing: -0.01em;
	color: var(--ip-ink);
}
.ip-release__summary { margin: 6px 0 0; font-size: var(--ip-font-sm); line-height: 1.5; color: var(--ip-label); }
.ip-release__changes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.ip-release__change {
	display: flex;
	align-items: baseline;
	gap: 10px;
	font-size: var(--ip-font-sm);
	line-height: 1.5;
	color: var(--ip-ink-soft);
}
/* One width for all three, so the sentences beside them start in a column. */
.ip-release__kind { flex: 0 0 72px; text-align: center; }

/* -----------------------------------------------------------------------------
   Pit display — a showcase loop for a screen nobody is sitting at

   templates/display/player.php, driven by pages/display-player.js. Always the
   dark palette: it is drawn for a TV or a projector, where white glares.

   Everything sits on a fixed 1920×1080 stage that is scaled to the screen with
   one transform (--show-scale, set by the player), so the composition is the
   same on a 4K TV and a 720p projector and nothing reflows between them. Sizes
   inside it are in stage pixels and its own type scale, --ip-show-font-*.
   -------------------------------------------------------------------------- */

/* The list of displays, at /display — an ordinary portal page, so ordinary sizes. */
.ip-display-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.ip-display-card { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
.ip-display-card__head { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ip-display-card__icon {
	flex: 0 0 36px;
	height: 36px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 10px;
	background: var(--ip-mint);
	color: var(--ip-teal);
	font-size: var(--ip-font-h3);
}
.ip-display-card__title { min-width: 0; }
.ip-display-card__name {
	font-size: var(--ip-font-base);
	font-weight: 600;
	letter-spacing: -0.01em;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.ip-display-card__preset { font-size: var(--ip-font-xs); color: var(--ip-muted); }
.ip-display-card__facts { display: flex; flex-direction: column; gap: 2px; }
.ip-display-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: auto; }

/* One display's slides, at /display/edit. Each row borrows the list editor's
   card, pill and remove (.ip-lists__item) — same gestures as every other list
   the organisation puts in order — and adds what a slide has: what it shows,
   and its time on screen. */
.ip-slides { display: flex; flex-direction: column; gap: 6px; padding: 14px 18px 18px; }
/* Beside a heading that wraps on a phone; the button keeps its one line. */
.ip-slides-add { flex: 0 0 auto; white-space: nowrap; }
.ip-slide { padding-top: 6px; padding-bottom: 6px; }
.ip-slide__glyph {
	flex: 0 0 30px;
	height: 30px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 8px;
	background: var(--ip-mint);
	color: var(--ip-teal);
	font-size: var(--ip-font-sm);
}
.ip-slide__about { flex: 1 1 0; min-width: 0; }
.ip-slide__name { font-size: var(--ip-font-sm); font-weight: 600; }
.ip-slide__hint { font-size: var(--ip-font-xs); color: var(--ip-muted); line-height: 1.45; text-wrap: pretty; }
/* The own slide with nothing in it: it saves, but the screen will skip it. */
.ip-slide__hint.is-warning { color: var(--ip-amber); }
.ip-slide__time {
	display: flex;
	align-items: center;
	gap: 4px;
	font-size: var(--ip-font-xs);
	color: var(--ip-muted);
	margin: 0;
}
/* Wide enough for "60" and the browser's spinner, no wider: the column of
   times reads down the list. */
.ip-slide__time .ip-input {
	width: 66px;
	padding-top: 4px;
	padding-bottom: 4px;
	text-align: right;
	font-variant-numeric: tabular-nums;
}
/* The title that keeps a loop saying whose screen it is cannot be turned off
   or removed; its controls stay in the column and say why on hover. */
.ip-slide .ip-lists__remove:disabled { opacity: 0.45; cursor: default; box-shadow: none; }
.ip-slide .ip-lists__remove:disabled:hover { color: var(--ip-faint); }

@media (max-width: 640px) {
	/* What it shows wraps under its name; the time and switches drop to a line
	   of their own rather than squeezing the hint to a word a line. */
	.ip-slide { flex-wrap: wrap; }
	.ip-slide .ip-lists__meta { flex-basis: 100%; justify-content: flex-end; }
}


.ip-show-body {
	overflow: hidden;
	user-select: none;
	-webkit-user-select: none;
}

.ip-show {
	position: fixed;
	inset: 0;
	overflow: hidden;
}
.ip-show.is-idle { cursor: none; }

.ip-show__stage {
	position: absolute;
	left: 50%;
	top: 50%;
	width: 1920px;
	height: 1080px;
	display: flex;
	flex-direction: column;
	transform: translate(-50%, -50%) scale(var(--show-scale, 1));
}

.ip-show__slides { position: relative; flex: 1; min-height: 0; }

.ip-show-slide {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	padding: 72px 96px 32px;
	opacity: 0;
	transition: opacity 700ms ease;
}
.ip-show-slide.is-in { opacity: 1; }
.ip-show-slide.is-out { opacity: 0; }
/* What arrives rises a little as it fades in; what leaves only fades. */
.ip-show-slide > * {
	transform: translateY(20px);
	transition: transform 900ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.ip-show-slide.is-in > *,
.ip-show-slide.is-out > * { transform: none; }

/* ---- Heading ---- */

.ip-show-head { flex: 0 0 auto; }
.ip-show-eyebrow {
	font-size: var(--ip-show-font-meta);
	font-weight: 700;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--ip-teal);
}
.ip-show-title {
	margin: 10px 0 0;
	font-size: var(--ip-show-font-head);
	font-weight: 800;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--ip-ink);
}

/* ---- Title ---- */

.ip-show-cover {
	flex: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
}
.ip-show-cover__logo {
	width: 220px;
	height: 220px;
	object-fit: contain;
	background: var(--ip-logo-ground);
	border-radius: 32px;
	padding: 18px;
	margin-bottom: 48px;
	box-shadow: 0 24px 64px var(--ip-shadow-3);
}
.ip-show-cover__name {
	margin: 0;
	max-width: 1600px;
	font-size: var(--ip-show-font-title);
	font-weight: 800;
	letter-spacing: -0.03em;
	line-height: 1.05;
	color: var(--ip-ink);
}
.ip-show-cover__line {
	margin-top: 24px;
	font-size: var(--ip-show-font-body);
	color: var(--ip-muted);
}
.ip-show-cover__teams {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 20px;
	margin-top: 64px;
}
.ip-show-cover__team {
	display: flex;
	align-items: center;
	gap: 16px;
	padding: 14px 24px;
	border-radius: 20px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
}
.ip-show-cover__team-name {
	font-size: var(--ip-show-font-meta);
	font-weight: 600;
	color: var(--ip-ink-soft);
	max-width: 420px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Team marks, at the stage's size. */
.ip-show-avatar { width: 56px; height: 56px; border-radius: 12px; padding: 4px; }
.ip-show-avatar--lg { width: 72px; height: 72px; border-radius: 14px; }
/* FIRST's colour solid, unlike .ip-prog everywhere else: a tint is lost from
   across a pit, and this is read from there. */
.ip-show-prog {
	font-size: var(--ip-show-font-meta);
	padding: 6px 14px;
	border-radius: 10px;
	background: var(--prog, var(--ip-rule));
	color: var(--prog-fg, var(--ip-muted));
	border-color: transparent;
}
.ip-show-tag {
	font-size: var(--ip-show-font-meta);
	padding: 8px 18px;
	border-radius: 12px;
}

/* ---- Numbers ---- */

.ip-show-numbers {
	flex: 1;
	display: grid;
	grid-template-columns: 1.1fr 1fr;
	align-items: center;
	gap: 72px;
}
.ip-show-hero__value {
	font-size: var(--ip-show-font-figure);
	font-weight: 800;
	letter-spacing: -0.04em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	color: var(--ip-teal);
}
.ip-show-hero__label {
	margin-top: 12px;
	font-size: var(--ip-show-font-head);
	font-weight: 600;
	color: var(--ip-ink-soft);
}
.ip-show-tiles {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 28px;
}
.ip-show-tile {
	padding: 32px 36px;
	border-radius: 24px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
}
.ip-show-tile__value {
	font-size: var(--ip-show-font-title);
	font-weight: 700;
	letter-spacing: -0.03em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	color: var(--ip-ink);
}
.ip-show-tile__label {
	margin-top: 12px;
	font-size: var(--ip-show-font-meta);
	color: var(--ip-muted);
}

/* ---- Months ---- */

.ip-show-bars {
	flex: 1;
	min-height: 0;
	display: flex;
	gap: 14px;
	margin-top: 48px;
}
.ip-show-bars.is-dense { gap: 4px; }
.ip-show-bar {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
}
.ip-show-bar__value,
.ip-show-bar__label {
	flex: 0 0 48px;
	display: flex;
	align-items: center;
	justify-content: center;
	white-space: nowrap;
	font-size: var(--ip-show-font-meta);
	font-variant-numeric: tabular-nums;
}
.ip-show-bar__value { align-items: flex-end; padding-bottom: 8px; color: var(--ip-ink-soft); font-weight: 600; }
.ip-show-bar__label { color: var(--ip-muted); }
.ip-show-bar__track { position: relative; flex: 1; }
.ip-show-bar__fill {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: var(--h);
	min-height: 4px;
	border-radius: 10px 10px 4px 4px;
	background: var(--ip-teal-pale);
	transform: scaleY(0);
	transform-origin: bottom;
	transition: transform 900ms cubic-bezier(0.2, 0.7, 0.2, 1) calc(var(--i, 0) * 35ms + 200ms);
}
.ip-show-bar__fill.is-peak { background: var(--ip-teal); }
.is-in .ip-show-bar__fill,
.is-out .ip-show-bar__fill { transform: none; }

/* ---- Types ---- */

.ip-show-meters {
	flex: 1;
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: 32px;
	margin-top: 40px;
}
.ip-show-meter { display: flex; align-items: center; gap: 28px; }
.ip-show-meter__glyph {
	flex: 0 0 80px;
	height: 80px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 18px;
	font-size: var(--ip-show-font-meta);
	font-weight: 700;
}
.ip-show-meter__body { flex: 1; min-width: 0; }
.ip-show-meter__head {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: 24px;
	margin-bottom: 14px;
}
.ip-show-meter__label { font-size: var(--ip-show-font-body); font-weight: 600; color: var(--ip-ink); }
.ip-show-meter__value { font-size: var(--ip-show-font-meta); color: var(--ip-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ip-show-meter__track { height: 22px; border-radius: 11px; background: var(--ip-track); overflow: hidden; }
.ip-show-meter__fill {
	height: 100%;
	width: var(--w);
	border-radius: 11px;
	background: var(--ip-teal-pale);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 900ms cubic-bezier(0.2, 0.7, 0.2, 1) calc(var(--i, 0) * 90ms + 200ms);
}
.ip-show-meter__fill.is-peak { background: var(--ip-teal); }
.is-in .ip-show-meter__fill,
.is-out .ip-show-meter__fill { transform: none; }

/* ---- Spotlight ---- */

.ip-show-spot {
	flex: 1;
	min-height: 0;
	display: grid;
	grid-template-columns: 52% 1fr;
	gap: 72px;
}
/* No photo: the type's own glyph holds the picture's place, so the slide keeps its shape. */
.ip-show-spot.is-text { grid-template-columns: 36% 1fr; }
.ip-show-spot__panel {
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 28px;
	font-size: var(--ip-show-font-figure);
	font-weight: 800;
	letter-spacing: -0.02em;
}
.ip-show-spot__panel.is-neutral { background: var(--ip-surface); color: var(--ip-muted); border: 1px solid var(--ip-border); }
.ip-show-spot__photo {
	position: relative;
	border-radius: 28px;
	overflow: hidden;
	background: var(--ip-surface);
}
.ip-show-spot__photo img,
.ip-show-mosaic__item img,
.ip-show-custom__image img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}
/* A slow drift across a still, for as long as the slide is up. */
.ip-show-spot__photo img,
.ip-show-mosaic__item img {
	animation: ip-show-drift var(--slide-ms, 12s) linear both;
}
@keyframes ip-show-drift {
	from { transform: scale(1.02); }
	to   { transform: scale(1.1) translate(-1.5%, -1%); }
}
.ip-show-spot__body {
	min-width: 0;
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: 22px;
}
.ip-show-spot__meta {
	display: flex;
	align-items: center;
	gap: 20px;
	font-size: var(--ip-show-font-meta);
	color: var(--ip-muted);
}
.ip-show-spot__name {
	margin: 0;
	font-size: var(--ip-show-font-head);
	font-weight: 800;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--ip-ink);
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;
}
.ip-show-spot__where { font-size: var(--ip-show-font-meta); color: var(--ip-label); }
.ip-show-spot__text {
	margin: 0;
	font-size: var(--ip-show-font-body);
	line-height: 1.45;
	color: var(--ip-ink-soft);
	display: -webkit-box;
	-webkit-line-clamp: 5;
	-webkit-box-orient: vertical;
	overflow: hidden;
}
.ip-show-spot__stats { display: flex; gap: 64px; margin-top: 8px; }
.ip-show-spot__figure {
	font-size: var(--ip-show-font-title);
	font-weight: 700;
	letter-spacing: -0.03em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	color: var(--ip-teal);
}
.ip-show-spot__label { margin-top: 8px; font-size: var(--ip-show-font-meta); color: var(--ip-muted); }
.ip-show-spot__names {
	font-size: var(--ip-show-font-meta);
	line-height: 1.4;
	color: var(--ip-label);
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/* ---- Photos ---- */

.ip-show-slide--photos { padding-top: 48px; }
.ip-show-mosaic {
	flex: 1;
	min-height: 0;
	display: grid;
	gap: 20px;
}
.ip-show-mosaic--2 { grid-template-columns: 1fr 1fr; }
.ip-show-mosaic--3 { grid-template-columns: 1.4fr 1fr; grid-template-rows: 1fr 1fr; }
.ip-show-mosaic--3 .ip-show-mosaic__item:first-child { grid-row: span 2; }
.ip-show-mosaic--4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.ip-show-mosaic--6 { grid-template-columns: repeat(3, 1fr); grid-template-rows: 1fr 1fr; }
.ip-show-mosaic__item {
	position: relative;
	margin: 0;
	min-height: 0;
	border-radius: 24px;
	overflow: hidden;
	background: var(--ip-surface);
	opacity: 0;
	transition: opacity 800ms ease calc(var(--i, 0) * 140ms + 150ms);
}
.is-in .ip-show-mosaic__item,
.is-out .ip-show-mosaic__item { opacity: 1; }
.ip-show-mosaic__item figcaption {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: 56px 24px 18px;
	background: linear-gradient(transparent, var(--ip-scrim));
	font-size: var(--ip-show-font-meta);
	font-weight: 600;
	color: var(--ip-ink);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ---- Teams ---- */

.ip-show-teams {
	flex: 1;
	min-height: 0;
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	grid-auto-rows: min-content;
	align-content: center;
	gap: 24px;
	margin-top: 40px;
}
.ip-show-team {
	min-width: 0;
	padding: 26px 28px;
	border-radius: 24px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	opacity: 0;
	transition: opacity 600ms ease calc(var(--i, 0) * 60ms + 150ms);
}
.is-in .ip-show-team,
.is-out .ip-show-team { opacity: 1; }
.ip-show-team__top { display: flex; align-items: center; gap: 16px; min-height: 72px; }
.ip-show-team__name {
	margin-top: 16px;
	font-size: var(--ip-show-font-meta);
	font-weight: 700;
	color: var(--ip-ink);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.ip-show-team__meta { margin-top: 6px; font-size: var(--ip-show-font-meta); color: var(--ip-muted); font-variant-numeric: tabular-nums; }

/* ---- The organisation's own slide ---- */

.ip-show-custom {
	flex: 1;
	min-height: 0;
	display: grid;
	grid-template-columns: minmax(0, 1400px);
	justify-content: center;
	align-items: center;
	gap: 96px;
}
.ip-show-custom.has-qr { grid-template-columns: minmax(0, 1fr) auto; }
.ip-show-custom.has-image { grid-template-columns: 0.9fr 1fr; gap: 72px; }
.ip-show-custom__image { height: 100%; border-radius: 28px; overflow: hidden; background: var(--ip-surface); }
.ip-show-custom__body { min-width: 0; display: flex; flex-direction: column; gap: 28px; }
.ip-show-custom.has-image.has-qr .ip-show-custom__body { gap: 36px; }
.ip-show-custom__heading {
	margin: 0;
	font-size: var(--ip-show-font-title);
	font-weight: 800;
	letter-spacing: -0.03em;
	line-height: 1.05;
	color: var(--ip-ink);
}
.ip-show-custom__text {
	margin: 0;
	font-size: var(--ip-show-font-body);
	line-height: 1.45;
	color: var(--ip-ink-soft);
	white-space: pre-line;
}
.ip-show-qr { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.ip-show-custom.has-image .ip-show-qr { flex-direction: row; gap: 28px; align-items: center; }
.ip-show-qr__plate {
	width: 420px;
	height: 420px;
	padding: 12px;
	border-radius: 28px;
	background: var(--ip-logo-ground);
}
.ip-show-custom.has-image .ip-show-qr__plate { width: 260px; height: 260px; border-radius: 20px; }
.ip-show-qr__plate svg { width: 100%; height: 100%; display: block; fill: var(--ip-code-ink); }
.ip-show-qr__link {
	font-size: var(--ip-show-font-meta);
	font-weight: 600;
	color: var(--ip-label);
	max-width: 520px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ---- Footer ---- */

.ip-show__foot {
	position: relative;
	flex: 0 0 96px;
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: 32px;
	padding: 0 96px;
	font-size: var(--ip-show-font-meta);
	color: var(--ip-muted);
}
.ip-show-foot__org { display: flex; align-items: center; gap: 16px; min-width: 0; }
.ip-show-foot__org img {
	width: 48px;
	height: 48px;
	object-fit: contain;
	border-radius: 10px;
	padding: 3px;
	background: var(--ip-logo-ground);
}
.ip-show-foot__org span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: var(--ip-label); }
.ip-show-foot__dots { display: flex; gap: 12px; }
.ip-show-foot__dots span { width: 12px; height: 12px; border-radius: 50%; background: var(--ip-track); }
.ip-show-foot__dots span.is-on { background: var(--ip-teal); }
.ip-show-foot__clock { justify-self: end; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ip-label); }
.ip-show-foot__progress {
	position: absolute;
	left: 96px;
	right: 96px;
	top: 0;
	height: 4px;
	border-radius: 2px;
	background: var(--ip-track);
	overflow: hidden;
}
.ip-show-foot__progress::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--ip-teal-pale);
	transform-origin: left;
	animation: ip-show-progress var(--slide-ms, 10s) linear both;
}
.ip-show-foot__progress.is-paused::after { animation-play-state: paused; }
@keyframes ip-show-progress {
	from { transform: scaleX(0); }
	to   { transform: scaleX(1); }
}

/* ---- Before the loop, and when it has stopped ---- */

.ip-show__notice {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 24px;
	opacity: 0;
	pointer-events: none;
	transition: opacity 300ms ease;
}
.ip-show__notice.is-on { opacity: 1; pointer-events: auto; }
.ip-show__notice-card {
	max-width: 560px;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 12px;
	text-align: center;
}
.ip-show__notice-icon { font-size: var(--ip-font-h1); color: var(--ip-muted); }
.ip-show__notice-title { font-size: var(--ip-font-h1); font-weight: 700; color: var(--ip-ink); }
.ip-show__notice-body { font-size: var(--ip-font-base); line-height: 1.5; color: var(--ip-muted); }
.ip-show__spinner {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	border: 3px solid var(--ip-track);
	border-top-color: var(--ip-teal);
	animation: ip-show-spin 900ms linear infinite;
}
@keyframes ip-show-spin { to { transform: rotate(360deg); } }

/* The controls are for whoever is setting the screen up, and leave with the
   pointer — a TV left alone shows only the slides. */
.ip-show__controls {
	position: absolute;
	top: 20px;
	right: 20px;
	display: flex;
	align-items: center;
	gap: 14px;
	transition: opacity 300ms ease;
}
.ip-show.is-idle .ip-show__controls { opacity: 0; pointer-events: none; }
.ip-show.is-fullscreen #show-fullscreen { display: none; }
#show-fullscreen { white-space: nowrap; }
.ip-show__hint { font-size: var(--ip-font-xs); color: var(--ip-faint); }
@media (max-width: 600px) { .ip-show__hint { display: none; } }

/* Paused by whoever is at the keyboard: said once, quietly, in the footer's place. */
.ip-show.is-paused .ip-show-foot__dots::after {
	content: 'Paused';
	margin-left: 8px;
	line-height: 12px;
	color: var(--ip-amber);
}

@media (prefers-reduced-motion: reduce) {
	.ip-show-slide,
	.ip-show-slide > *,
	.ip-show-bar__fill,
	.ip-show-meter__fill,
	.ip-show-mosaic__item,
	.ip-show-team { transition-duration: 1ms; transition-delay: 0s; }
	.ip-show-spot__photo img,
	.ip-show-mosaic__item img { animation: none; }
}

/* -----------------------------------------------------------------------------
   Hover

   One treatment for every neutral control and row: --ip-veil-hover laid over
   whatever the thing already is. Each of these used to pick a neighbouring
   surface token for its hover instead — --ip-rule for a button, --ip-bg for a
   quiet one, --ip-surface-sunk for a row — and those sit on different sides of
   a card in the dark theme: --ip-bg is darker than --ip-surface, --ip-rule and
   --ip-surface-sunk are lighter. So two buttons side by side moved in opposite
   directions under the pointer. The veil is the one token defined as "hover",
   and it inverts per theme on purpose.

   A wash over the background rather than a background, so it works on a solid
   button, a transparent one and a row alike, and Bootstrap's
   `bg-transparent !important` (the account menu, the sidebar foot) cannot
   cancel it. `:where` keeps it at zero weight: a control with a hover of its
   own — primary, danger, upgrade, a link — sets `background` and wins, and a
   selected chip or segment is excluded rather than washed.

   Not here, deliberately: tinted controls deepen their own tint (the evidence
   chip, a combo token), and choice cards outline in the accent (.ip-type and
   its kin). Each of those families is consistent within itself.
   -------------------------------------------------------------------------- */

@media (hover: hover) {
	:where(.ip-btn, .ip-theme-toggle, .ip-notif__toggle, .ip-notif__item, .app-topbar__menu, .ip-flyout__close, .ip-auth__reveal,
	       .sidebar-foot-btn, .account-item, .ip-section-toggle, .ip-settings__link,
	       .ip-table__row, .ip-evidence-row,
	       #sidebar .nav-link:not(.active),
	       .ip-chip:not(.is-active),
	       .ip-seg__btn:not(.is-active), .ip-lists__tab:not(.is-active)):hover {
		background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover));
	}
}

/* -----------------------------------------------------------------------------
   Press

   After every component on purpose: a press has to win over the hover rule the
   same control carries, and those are written at the same weight.
   -------------------------------------------------------------------------- */

/* Pressed. Feedback lands on the press, not on the release that follows it, and
   it is the same small give everywhere so a control feels like one family. On a
   touch screen this is the only acknowledgement there is: hover never happens.
   `scale` rather than `transform`, so it composes with any transform a control
   already wears, and an instant press with a quick release — the press is the
   finger arriving, and that is not something to ease into.

   `will-change` while pressed puts the control on a compositor layer, which is
   drawn once at full size and shrunk whole. Without it the control is repainted
   at 96%, and each thing inside it re-snaps to the pixel grid on its own rule:
   a team avatar's 1px white frame became a 0.96px band smeared grey across two
   pixels, its `pixelated` logo lost a whole row and column of its drawing, and
   the text moved by a different fraction than the picture beside it — so a
   held button's contents visibly slid against one another. Set on :active only,
   not at rest, so a page of buttons is not a page of layers; the layer is drawn
   at full size even when it arrives in the same frame as the scale. */
:is(.ip-btn, .ip-chip, .ip-theme-toggle, .ip-notif__toggle, .ip-flyout__close, .app-topbar__menu,
    .ip-type, .ip-type-pill, .ip-output, .ip-team-card, .ip-picker-btn, .ip-attend__cell,
    .ip-check, .ip-evidence-chip, .sidebar-foot-btn,
    .gal-icon-btn, .ip-auth__reveal):not(:disabled) {
	transition-property: scale, background-color, border-color, color, box-shadow, opacity;
	transition-duration: 160ms;
	transition-timing-function: ease-out;
}
:is(.ip-btn, .ip-chip, .ip-theme-toggle, .ip-notif__toggle, .ip-flyout__close, .app-topbar__menu,
    .ip-picker-btn, .ip-attend__cell, .ip-check, .ip-evidence-chip,
    .sidebar-foot-btn, .gal-icon-btn, .ip-auth__reveal):not(:disabled):active {
	scale: 0.96;
	will-change: scale;
	transition-duration: 0s;
}
/* Large targets — cards and tiles — give less, or a whole card visibly shrinks. */
:is(.ip-type, .ip-type-pill, .ip-output, .ip-team-card):active { scale: 0.985; will-change: scale; transition-duration: 0s; }
/* Rows and menu entries are the width of their container, where any scale reads
   as the layout wobbling; they take a second veil instead, a step past their
   hover in either theme. */
:is(.ip-settings__link, .ip-table__row.is-clickable, button.ip-table__row,
    .ip-lists__tab, .ip-combo__option, .fm-suggest-option,
    .ip-section-toggle, .ip-section__toggle, .ip-link-pick, .ip-evidence-row, .account-item,
    .dropdown-item):active {
	background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)),
	                  linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover));
}
/* The segmented control's pill is a separate element behind the label and would
   not shrink with it, so the label dims instead — the platform's own answer. */
.ip-seg__btn:active { opacity: 0.6; }
#sidebar :is(.nav-link, button.nav-link):not(.active):active {
	background-image: linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover)),
	                  linear-gradient(var(--ip-veil-hover), var(--ip-veil-hover));
}
#sidebar .nav-link.active:active { background-color: var(--ip-teal-dark); }

/* -----------------------------------------------------------------------------
   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 and badges. 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);

	/* Inline <code> — a domain, a record — was Bootstrap's pink, the one
	   colour on those screens from outside the palette. */
	--bs-code-color:      var(--ip-ink-soft);

	--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);
}
/* Every dropdown eases in. Popper places a menu with an inline transform, which
   an animation on transform would replace for its duration and so send the menu
   to the wrong place; `translate` is its own property and composes with it. */
.dropdown-menu.show { animation: ip-pop-in 160ms ease-out; }
@keyframes ip-pop-in {
	from { opacity: 0; translate: 0 -4px; }
	to   { opacity: 1; translate: none; }
}
@media (prefers-reduced-motion: reduce) {
	.dropdown-menu.show { animation: none; }
}
.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);
}

/* 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: var(--ip-font-h2); font-weight: 600; letter-spacing: -0.01em; }
/* Full opacity: the scrim token is already translucent, and an opacity on top
   multiplied the two — a dialog dimmed the page to about a third of what the
   entry sheet does, though a dialog is the one that blocks.
   Set through Bootstrap's own variables, on the backdrop in every state: the
   colour used to be on `.show` alone, and closing drops `.show` before the
   fade-out, so for those 150ms the backdrop fell back to Bootstrap's solid
   black and the page flashed dark in both themes. */
.modal-backdrop {
	--bs-backdrop-bg: var(--ip-scrim);
	--bs-backdrop-opacity: 1;
}
/* 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; }
/* The same small rise .ip-modal makes. The two look identical, so they arrive
   identically; this scaled up from the middle while that one rose. */
.modal.fade .modal-dialog { transform: translateY(6px); transition: transform 0.2s ease, opacity 0.15s ease; }
.modal.fade.show .modal-dialog { transform: none; }
@media (prefers-reduced-motion: reduce) {
	.modal.fade .modal-dialog { transform: none; }
}
.modal.modal-static .modal-dialog { transform: scale(1.02) !important; }

/* Buttons */
.btn {
	border-radius: 9px;
	font-size: var(--ip-font-sm);
	font-weight: 500;
	padding: 9px 15px;
}
.btn-sm { padding: 6px 11px; font-size: var(--ip-font-xs); border-radius: 8px; }
/* Nothing of ours is a .btn any more; Swagger UI's buttons on the API docs
   are, and these two rules are what still shape them. */

.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: var(--ip-font-xs);
	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-card--flush::after { display: none; }
	.ip-doc { max-width: none; padding: 0; }
	body { background: #ffffff; }
}

/* Laptop widths: the side cards that sit beside a main block at full width, and
   fill the row under it when they wrap — rather than keep a rail's width and
   leave the other half of the row empty. */
.ip-risk-stand { max-width: 330px; }
.ip-award-picker { max-width: 300px; }
@container (max-width: 763px) {
	.ip-report-side { max-width: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}
@container (max-width: 759px) {
	.ip-award-picker { max-width: none; }
	/* Fifteen awards stacked ahead of the one picked put its detail a screen and
	   a half down. They scroll in their own box instead, and stay all in reach. */
	.ip-award-picker #aw-list { max-height: var(--aw-list-h, 320px); overflow-y: auto; padding-right: 4px; }
}
@media (max-width: 1100px) {
	.ip-risk-stand { max-width: none; }
}

/* The map's layer switch sits in a toolbar of 35px controls like Teams' and
   Evidence's; at the bar's 41px it made the row, and the search with it, taller
   than every sibling page. */
#map-layer { height: 35px; align-items: stretch; }
#map-layer .ip-seg__btn { padding-block: 0; }
