/* Buffet -- the application shell.
 *
 * The palette and type are the upload and item pages' own (app/public/item.php), extended rather
 * than replaced: a dev environment that looks assembled from parts reads as assembled from parts.
 * The type is now Instrument Sans, declared once in assets/fonts.css and asked for here as
 * --font-ui; this file states no font stack of its own, and neither should any other. That replaces
 * the system-font position this header used to argue for -- the reference points for the LAYOUT are
 * still Finder and Photos, but the lettering is Buffet's own rather than the host OS's.
 *
 * Three structural decisions worth reading before editing:
 *
 *   1. The shell is a grid with an internally-scrolling sidebar and main region. The page itself
 *      never scrolls, so the sidebar stays put, the toolbar stays put, and a grid of five hundred
 *      thumbnails does not carry the navigation off the top of the screen.
 *   2. Every state that matters is a data- attribute on an element, never a class the JS invents:
 *      data-busy, data-tone, data-selected, data-kind. Styling reads as a description of state and
 *      the JS has one way to change appearance.
 *   3. Focus is never removed, only replaced. :focus-visible rings are deliberate and inset so that
 *      keyboard navigation is a first-class way to use this (§17 lists keyboard control as a stated
 *      requirement, not a nicety).
 */

:root
{
	/* THE ONE DECLARATION THAT MAKES NATIVE CONTROLS OBEY THE PALETTE, and its absence was a real
	   bug: text inputs, textareas and selects rendered blinding white in dark mode while everything
	   around them was dark. The palette below is only custom properties, and a custom property tells
	   the browser nothing about which way round the page is -- so the UA went on painting every
	   control it draws itself with its LIGHT appearance. `color-scheme` is how a page says which,
	   and it reaches the parts CSS cannot: a <select>'s dropdown list is an OS menu, and so are the
	   date picker, the autofill highlight, the spin buttons and the scrollbars. Styling a control's
	   own box, which the rule further down now does, fixes none of those on its own.

	   `light dark` rather than a bare `dark`: the page follows the person's setting, exactly as the
	   prefers-color-scheme block below does, so the two can never disagree.

	   THROUGH A TOKEN, since D351, and that indirection is the whole of what lets a theme be dark
	   in BOTH schemes. A theme owns custom properties and nothing else -- that is the rule that
	   keeps a theme from disagreeing with the design about anything but colour -- so anything
	   scheme-dependent that is NOT a custom property is, by construction, out of a theme's reach.
	   There were exactly three such things in the shell, and each one broke a dark-in-light-mode
	   theme in a way no palette could repair: this declaration, and the two logo rules below. So
	   all three are read from tokens now, they keep their present behaviour by default, and a
	   theme that goes dark against the scheme sets them. See `deferred.md` C50 and D351. */
	color-scheme: var( --ua-color-scheme );

	/* What the UA paints for the parts CSS cannot reach. `light dark` means "follow the person";
	   a bare `dark` means "this page is dark whatever the OS says", which is what a dark-in-both
	   theme needs and what nothing else in this sheet can say for it. */
	--ua-color-scheme: light dark;

	/* Which of the two logo files in the markup is the one on screen. Defaults below, swapped by
	   the dark block, overridden wholesale by a theme that is dark in both schemes. */
	--logo-light-display: block;
	--logo-dark-display: none;
	/* The neon layer over the wordmark is drawn for the dark logo; the light scheme inverts it. */
	--logo-neon-filter: invert( 1 );
	/* What paints through the neon mask (.sidebar-wordmark::after), and how it meets the neon.
	   `none` for no overlay. */
	/* The gradient ENDS on the colour it starts with, so that as it slides it tiles without a seam
	   (@keyframes logo-neon-drift). One full pass takes --logo-neon-drift-duration. */
	--logo-neon-overlay: linear-gradient( 90deg, #ff2d55, #ff9500 14%, #ffd60a 28%, #34c759 43%, #00c7ff 57%, #3a5cff 71%, #bf5af2 86%, #ff2d55 );
	--logo-neon-drift-duration: 16s;
	--logo-neon-overlay-blend: normal;
	/* The logo row's hover: the neon lit bright turquoise, with a glow of the same colour, in
	   place of the row-hover background every other sidebar row uses. */
	--logo-neon-hover-overlay: #00f5e1;
	--logo-neon-hover-glow: drop-shadow( 0 0 .25rem #00f5e1 ) drop-shadow( 0 0 .6rem rgb( 0 245 225 / .55 ) );

	--page-background: #f4f4f2;
	--page-background-translucent: rgb( 244 244 242 / 0.2 );
	--card-background: #ffffff;
	--ink: #16181c;
	--ink-quiet: #5c626c;
	/* A THIRD RUNG ON THE INK LADDER, AND THE ONE BELOW THE LEGIBILITY FLOOR (Jon, 2026-09-21).
	   `--ink` is the text, `--ink-quiet` is the text read second and still holds AA at about 6:1,
	   and this is a mark read only by somebody who looks for it: a separator glyph, a placeholder,
	   a hint that must not compete with the card it sits on. It measures ~2.2:1 on
	   `--card-background`, which is FAR below AA and is the point of the token -- so never a
	   sentence that carries meaning, never a control's only label, never the sole way anything is
	   said. Anything a person has to read is `--ink-quiet` or above.

	   Jon gave the dark value; every other one in this sheet and in the eight theme files is that
	   theme's own `--ink-quiet` mixed toward its own card ground until it lands on the same 2.2:1,
	   so the rung means the same thing under every palette instead of being eight guesses. It is a
	   literal rather than a `color-mix()` of `--ink-quiet` for the reason every other palette token
	   is: a theme tunes its inks by hue against its own surfaces (see morning-pool.css), and a
	   derived value would take that choice away from the theme file. */
	--ink-very-quiet: #acafb4;
	--rule: #d8d8d4;
	--accent: #2f6f4f;
	--in-use: #2468d2;
	/* THE WEATHER (D662): the queue button from cool to hot -- a balmy in-use blue, then amber, then a
	   burnt orange, then a scorching dark red that carries white text. Literals, like every palette token,
	   so a theme can retune them against its own surfaces. */
	--weather-1: #2468d2;
	--weather-2: #c98a12;
	--weather-3: #d4561c;
	--weather-4: #8f1d14;
	--accent-quiet: #e6efe9;
	--warning: #8a2b1f;
	--warning-quiet: #f7e8e5;

	/* --accent-quiet AT 0.7, FOR THE PLACES THAT NEED THE TINT TO LET THE GROUND THROUGH -- a tint
	   over a photograph, over a card that is already coloured, or stacked over another tint, where
	   an opaque swatch would cover what it is meant to sit on.

	   IT IS DECLARED ONCE, HERE, AND DELIBERATELY NOT RESTATED BY ANY THEME OR BY THE DARK BLOCK,
	   which is the one thing worth knowing about it. Every other palette token in this file is a
	   literal and therefore has to be written again for every scheme and every theme; this one is
	   DERIVED, and a custom property's `var()` is substituted against the value that WON the cascade
	   on the element it is declared on. `:root` is where app.css, item.css, upload.css, a theme file
	   and the scratchpad all declare `--accent-quiet`, and the last one to do so is the one this
	   reads. So it tracks Riviera's sand, Smoothpaste's dark blue and the dark block's #23302a with
	   no further declarations, and it cannot drift out of step with them the way a tenth hand-copied
	   hex would. Adding it to a theme would not be harmless: it would freeze that theme's translucent
	   at whatever was pasted, which is the bug this shape exists to prevent.

	   `color-mix( ... , transparent )` rather than an `rgba()` or a relative-colour `/ 0.7`, because
	   the source is a hex token and this is the idiom already in the sheet (see the search-hit tint
	   further down). It is alpha in the ordinary sense: 70% of the colour, 30% of whatever is behind
	   it. */
	--accent-quiet-translucent: color-mix( in srgb, var( --accent-quiet ) 70%, transparent );

	/* THE THREE SEMANTIC TONES A CONTROL CAN WEAR, and they exist because `--accent` and
	   `--warning` were being asked to mean four things between them (Jon, 2026-09-04).

	   `--create` is for a control that BRINGS SOMETHING INTO EXISTENCE -- New folder, New project,
	   Add somebody, Add a passkey. It has to be a green that is not `--accent`, which is already a
	   green in both palettes here: a forest green beside the accent's blue-green reads as a
	   different decision rather than as a near-miss, which is the whole test a second hue has to
	   pass. 4.7:1 on white.

	   `--danger` is for a control that TAKES SOMETHING AWAY, and it is deliberately NOT `--warning`.
	   The two were one token and the collision was real: `--warning` also colours the "Suspended"
	   badge, the diagnostics counter and the stale-view notice, none of which is destructive and
	   none of which should turn red because Erase did. A tone that means "be careful" and a tone
	   that means "this deletes" are two vocabularies, and merging them makes the second one cheap.
	   Redder and colder than --warning, so the two are distinguishable when they sit on one screen.

	   `--favorite` is the golden orange that Favorites now own -- the lit star, the Favorite pill,
	   the sidebar section. Favorites used to be drawn in `--accent`, which put them in the same
	   colour as every submit button on the page: personal state and the page's next step are not
	   the same kind of fact and should not be the same colour. Gold because a star is gold
	   everywhere else a person has ever seen one, and because nothing else in this palette is warm
	   except the selection. 4.6:1 on white, which is what keeps the worded pill legible. */
	/* ONE HEIGHT FOR EVERY CONTROL -- a button, a text field, a select -- AND IT IS A SYSTEM RULE
	   (Jon, 2026-09-04: *"we can match height to other inputs and selects in general, as a system
	   rule"*). It exists because there were four, on one screenshot of one item page.

	   Measured there: the base pill -- Favorite, FYI, Download original, Archive, Remove, Erase --
	   stood 27.1px; `.details-save` had its own padding AND its own font-size and stood 35.2px; the
	   share panel's submit was given an explicit `height` so it would line up with the field beside
	   it and stood 37.6px; and the fields themselves were whatever their padding and font made them.
	   Each exception had a good local reason, and that is exactly how a vocabulary comes apart: four
	   sensible local decisions and no global one.

	   **The rule is that a control does not decide its own height.** A panel that needs its submit
	   to line up with a select no longer measures one against the other -- they both name this. The
	   share panel is the proof: its `--share-control-height` was a hand-measured 2.35rem, and it is
	   now an alias of this token, so the row lines up by construction rather than by coincidence.

	   2.35rem is that hand-measured number promoted to the system, and it is chosen rather than
	   averaged: it is the one value here that was already tuned against a real `<input>` at 1rem
	   with its own padding and borders, so fields keep the proportions they have and it is the
	   buttons that move. A smaller number would have made every text field in the project cramped in
	   order to keep the pills dense.

	   `min-height` and never `height`, everywhere it is used: a label or a value that wraps must
	   grow its control rather than spill out of it.

	   Two deliberate exceptions, each stated where it lives and each a genuinely different kind of
	   control rather than a special case of this one: `.control-pill-compact` (a control riding
	   inside a dense row, where a full-height pill would set the row's height) and
	   `.toolbar-controls` (the chrome strip, which is a band of its own and sizes to
	   --toolbar-height). Anything else that wants a different height is this rule being wrong. */
	--control-height: 2.35rem;

	/* THE SMALL ROUND BUTTON (D430): a card's star and kebab, and every × that puts something away
	   -- a tray, the item menu, a shelf entry, a toast, a keyword, the search field. Each had its
	   own size, between 1.15rem and 1.6rem with a glyph of .7rem to 1rem, and all of them were
	   small on a desktop and smaller than a fingertip on a phone. Two tokens, the box and the glyph
	   in it, and one touch block (above the `*` rule) raises both.
	   Not --control-height: that is the content vocabulary's pill, and these sit INSIDE rows, cards
	   and headers whose height they must not set. */
	--icon-button-size: 1.75rem;
	--icon-button-glyph: 1.1rem;

	--create: #2c6b34;
	--create-quiet: #e6f1e6;
	--danger: #a32219;
	--danger-quiet: #f9e6e4;
	--favorite: #9a6708;
	--favorite-quiet: #fbf0d8;

	/* Shell-specific additions. The sidebar is a shade cooler and flatter than the content area,
	   which is how every Mac sidebar since Leopard has separated navigation from document. */
	--sidebar-background: #dfdfdf;
	--sidebar-ink: #2a2d33;
	--selection-background: #d9dde0;
	--selection-accent: #2f6f4f;
	--shadow-card: 0 1px 2px rgba( 20, 22, 26, .06 ), 0 4px 12px rgba( 20, 22, 26, .04 );

	/* THE SIDEBAR'S WIDTH, IN THREE PARTS, AND THIS IS THE ONLY PLACE ANY OF THEM EXISTS.
	
	   `--sidebar-width-preference` is what the person dragged: the shell writes it onto <body> as an
	   inline style, from a row in `user_preferences`, and sidebar.js rewrites it live during a drag.
	   The other two bound it, and `.app-shell` clamps between them -- so the BROWSER enforces the
	   limits on every paint, and a stored value that is somehow out of range simply cannot draw a
	   sidebar out of range. A guard the code says no with, rather than an assertion that tells you
	   afterwards (CLAUDE.md §2).
	
	   The minimum is what the sidebar has always been, which is Jon's condition: the size it was is
	   the smallest it gets. preferences.php deliberately restates none of these numbers -- its
	   storage bounds are wider on purpose, and it explains why. */
	--sidebar-width-minimum: 16.5rem;
	--sidebar-width-maximum: 34rem;
	--sidebar-width-preference: var( --sidebar-width-minimum );
	--toolbar-height: 3.25rem;
	/* 2.25rem until 2026-09-11, when the tray buttons in it became drawn buttons and the row grew to
	   give them a margin (Jon). Everything docked above the footer -- the trays, the diagnostics
	   panel, the narrow screen's floating sidebar toggle -- reads this, so it moves with it. */
	--footer-height: 2.75rem;

	/* THE PHOTO GRID'S OWN MEASUREMENTS, AND THIS IS THE ONLY PLACE ANY OF THEM EXISTS (D272).

	   `--grid-tile-size` is what the person dragged: the shell writes it onto <body> from a row in
	   `user_preferences`, and app.js rewrites it live while the slider moves. It is deliberately NOT
	   declared here -- an unset custom property is what lets `var( --grid-tile-size,
	   var( --grid-tile-default ) )` fall through, so the default lives in exactly one place and the
	   server can say nothing at all when nobody has ever chosen. app.js reads --grid-tile-default
	   back off the computed style to place the slider's thumb, for the same reason.

	   The bounds a person can drag between are the slider's own `min` and `max`, which come from
	   preferences.php's storage range -- the one place this differs from the sidebar's width, where
	   the stylesheet clamps. A range input cannot be dragged out of its own bounds, so a second
	   clamp here would be a guard against nothing.

	   Two pixels of gutter rather than none: touching tiles read as one torn photograph, and two
	   pixels is enough to separate them without becoming a visible grid of lines. */
	--grid-tile-default: 160px;
	--grid-tile-gap: 2px;

	/* The ground a photo wall stands on, which is not --page-background and should not be. A contact
	   sheet wants a surface that says nothing: paper-white here, near-black in the dark palette, both
	   further from the middle than the shell's own background, so that what the eye picks up is the
	   pictures and not the wall. */
	--grid-ground: #ffffff;
	--grid-tile-empty: #ececeb;

	/* The lightbox is the same idea taken all the way: one picture, and nothing else lit. */
	--lightbox-ground: rgba( 12, 12, 14, .95 );
	--lightbox-ink: #f4f4f5;

	/* ONE HEARTBEAT FOR THE WHOLE INTERFACE, AND THIS IS THE ONLY PLACE THE TEMPO EXISTS.

	   Three surfaces say some version of "something is working, or something wants you": the
	   staleness badge in the toolbar, the footer's status dots, and -- as of this revision -- the
	   indeterminate upload bar in upload.css. They breathed at 2.4s, at 1.4s and at nothing
	   respectively: three unrelated tempos inside one corner of one screen, which the eye reads as
	   clutter rather than as a signal, because nothing tells it the three are the same kind of
	   statement.

	   Jon's brief for the rate, and it is the whole specification: *"not frantic, but noticeable."*
	   1.9s is slower than a resting pulse, which is what keeps it from reading as alarm, and fast
	   enough that a glance of under two seconds catches it moving.

	   `attention-pulse` below is the SHAPE; this token is the TEMPO. upload.css uses both, and says
	   so where it does -- keyframes are global, so one definition serves every sheet, and a second
	   copy of these numbers in the uploader's stylesheet is exactly the two-sources-for-one-fact
	   that CLAUDE.md §5 warns about. */
	--attention-rhythm: 1.9s;
}


/* The shape of every "this is live" beat in the interface. Opacity only, deliberately: it composites
   without layout, it survives on any background, and it means the same thing on a dot, on a pill and
   on a bar. Anything wanting motion INSTEAD of breathing (the uploader's sweeping band, the arrival
   flash) declares its own keyframe and only borrows the tempo. */
@keyframes attention-pulse
{
	0%, 100% { opacity: 1; }
	50%      { opacity: .35; }
}

@media ( prefers-color-scheme: dark )
{
	:root
	{
		/* The logo swap the media query used to perform directly, now expressed as the tokens the
		   rule reads (D351). Same behaviour, reachable by a theme. */
		--logo-light-display: none;
		--logo-dark-display: block;
		--logo-neon-filter: invert( 0 );

		--page-background: #17181a;
		--page-background-translucent: rgb( 23 24 26 / 0.2 );
		--card-background: #1f2124;
		--ink: #eceef1;
		--ink-quiet: #a2a8b2;
		--ink-very-quiet: #55575b;
		--rule: #32353a;
		--accent: #6fbf95;
		--in-use: #6ea8ff;
		--weather-1: #6ea8ff;
		--weather-2: #e0b04a;
		--weather-3: #f07a3e;
		--weather-4: #c0392b;
		--accent-quiet: #23302a;
		--warning: #f0907f;
		--warning-quiet: #33221f;

		/* The three semantic tones, after dark. Each is the light value lifted until it holds on
		   #1f2124 -- a card, which is the darkest ground any of the three lands on -- and each keeps
		   its hue, so a control does not change what it MEANS when the OS flips. The gold in
		   particular has to stay gold rather than drifting to yellow: a yellow star on a dark card
		   reads as a highlight, and a lit favorite is not a highlight. */
		--create: #79c98a;
		--create-quiet: #1d2c21;
		--danger: #f08a80;
		--danger-quiet: #33201e;
		--favorite: #e8b551;
		--favorite-quiet: #33290f;

		--sidebar-background: #121315;
		--sidebar-ink: #d6dae0;
		--selection-background: #2c3035;
		--selection-accent: #6fbf95;
		--shadow-card: 0 1px 2px rgba( 0, 0, 0, .4 );

		--grid-ground: #0b0c0d;
		--grid-tile-empty: #191a1c;
	}
}

/* The small round button under a finger (D430). Both media features, as the sidebar's disclosure
   slot does it: the sheet breakpoint catches a phone, `pointer: coarse` a tablet driven by touch.
   2.5rem is 40px -- short of the 44px a platform guideline asks of a primary control, and as much
   as a card's title line can give up to two of them. */
@media ( max-width: 46rem ), ( pointer: coarse )
{
	:root
	{
		--icon-button-size: 2.5rem;
		--icon-button-glyph: 1.35rem;
	}
}

*
{
	box-sizing: border-box;
}

html, body
{
	margin: 0;
}

/*
 * Locking the viewport is the SHELL's requirement, not this stylesheet's. The shell is a fixed grid
 * whose regions scroll independently, so its page must not scroll; a standalone page that merely
 * shares the palette -- the item view at its canonical /item.php URL -- must. Before this was
 * scoped, linking app.css from item.php made that page unscrollable below the fold, which is the
 * kind of breakage that looks like a rendering bug and is really a stylesheet claiming more
 * authority than it has.
 */
html { height: 100%; }

body.app-shell-body
{
	height: 100%;
	overflow: hidden;
}

body
{
	background: var( --page-background );
	color: var( --ink );
	font: var( --text-base )/1.5 var( --font-ui, system-ui, sans-serif );
	-webkit-font-smoothing: antialiased;
}

a
{
	color: var( --accent );
	text-decoration: none;
}

a:hover
{
	text-decoration: underline;
}


/* ---------------------------------------------------------------------------------------------
   TEXT-ENTRY CONTROLS, IN ONE PLACE.

   `color-scheme` on :root is what stops these being blinding white in dark mode; this is what makes
   them Buffet's white rather than the browser's grey. The two are not alternatives -- the first
   reaches the OS-drawn parts no stylesheet can touch, the second makes the box itself match the
   cards and panels it sits in, in BOTH schemes.

   It is here, as an element rule, because the bug it fixes was one of coverage rather than of
   design: `.toolbar-controls select` looked fine in dark mode purely because it happened to set a
   background and a colour, and every control that did not -- the item page's title and description,
   the role selects in People and in a folder's grants, the share panel's fields, the mention box --
   inherited the UA's light appearance. A per-surface fix would have left the next field somebody
   adds with the same defect, which is the shape of a rule that wants to be stated once.

   Scoped by TYPE rather than by `input`, and the omissions are the point: checkboxes, radios, file
   inputs and range sliders are controls whose whole appearance is the UA's, and giving them a
   padding and a background turns a checkbox into a small empty rectangle. Types not currently used
   in the markup (number, url, date) are listed anyway, so that adding such a field does not
   reintroduce this. Buttons are absent too -- `.control-pill` and its siblings own those.

   Anything that states its own background still wins on specificity: `.toolbar-controls select`,
   `.sidebar-search input` and the sign-in field are all more specific than a bare type selector, so
   this changes none of them and only reaches the controls that were never dressed at all.
   --------------------------------------------------------------------------------------------- */

input[ type="text" ],
input[ type="search" ],
input[ type="email" ],
input[ type="url" ],
input[ type="tel" ],
input[ type="number" ],
input[ type="password" ],
input[ type="date" ],
input[ type="datetime-local" ],
select,
textarea
{
	border: 1px solid var( --rule );
	border-radius: .35rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
}


/* THE OTHER HALF OF THE SYSTEM HEIGHT (--control-height, Jon 2026-09-04). The pill takes it and so
   does every single-line field, so a button, a text input and a select standing in one row line up
   because they name one number -- not because somebody measured them against each other, which is
   what the share panel used to do and what made its submit the tallest button in the project.

   `textarea` is deliberately NOT in this list, and it is the one omission that matters: a textarea
   is sized in `rows`, and a minimum height on it would silently floor a four-row box at the height
   of a one-row control. It keeps the border, the radius and the palette from the rule above, which
   is everything a textarea should share with a field and nothing it should share with a button.

   Checkboxes, radios, file inputs and range sliders are absent for the reason the rule above gives:
   their whole appearance is the UA's, and a height on a checkbox is a stretched checkbox.

   `min-height` clamps a stated `height`, so this WOULD reach into `.toolbar-controls`, whose select
   and button share an explicit 1.8rem. That strip is the declared exception -- a dense chrome band
   sized to --toolbar-height rather than to the content vocabulary -- so it restates its own minimum
   where it lives, and the exemption is visible at the place a person would look for it. */
input[ type="text" ],
input[ type="search" ],
input[ type="email" ],
input[ type="url" ],
input[ type="tel" ],
input[ type="number" ],
input[ type="password" ],
input[ type="date" ],
input[ type="datetime-local" ],
select
{
	min-height: var( --control-height );
}

/* A SELECT'S BOX IS DRAWN BY THE PAGE, NOT BY THE OS (Jon, 2026-09-05). The height rule above was
   true in Chrome and false in Safari: Safari keeps a native-appearance <select> at the height it
   chooses and ignores `min-height`, so the role select in a Members row sat a third shorter than
   the Remove beside it -- the system rule stated and not obeyed, on the browser the person who
   wrote the rule uses. `appearance: none` is the one declaration that makes Safari hand the box
   over. The dropdown LIST stays the OS menu (that is what `color-scheme` at the top reaches), which
   is the half nobody wants to redraw.

   What `appearance: none` takes away is the arrow, so it is put back as a background chevron, in a
   grey that reads on both palettes, standing in the right-hand padding. It is a `background-image`
   longhand, which means any later rule that writes the `background:` SHORTHAND wipes it -- so the
   two selects that do (the toolbar's and the sidebar's stand-in picker, both chrome rather than
   content) say `appearance: auto` where they live and keep the OS arrow. */
select
{
	appearance: none;
	-webkit-appearance: none;
	padding: .2rem 1.9rem .2rem .55rem;
	background-image: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%238a8f98' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 6l4 4 4-4'/%3E%3C/svg%3E" );
	background-repeat: no-repeat;
	background-position: right .6rem center;
	background-size: .7rem;
}

/* The placeholder is a prompt, not content, and at full --ink it reads as a value already typed. */
input::placeholder,
textarea::placeholder
{
	color: var( --ink-quiet );
}

/* A disabled field must still be legible -- the UA's disabled grey on a dark card is close to
   invisible. Dimmed against the page rather than the card, so it reads as inactive by sitting
   flatter than its neighbours rather than by losing its text. */
input:disabled,
select:disabled,
textarea:disabled
{
	/* The longhand, because the select rule above carries its chevron as a background-image and
	   the shorthand would take it with the colour. */
	background-color: var( --page-background );
	color: var( --ink-quiet );
	cursor: not-allowed;
}

:focus-visible
{
	outline: 2px solid var( --selection-accent );
	outline-offset: -2px;
	border-radius: .3rem;
}

.mono
{
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: .82em;
}

.quiet
{
	color: var( --ink-quiet );
}

.warn
{
	color: var( --warning );
}

.visually-hidden
{
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset( 50% );
	white-space: nowrap;
}


/* ---------------------------------------------------------------------------------------------
   The shell grid.
   --------------------------------------------------------------------------------------------- */

.app-shell
{
	/* Declared HERE and not on :root, which matters and is easy to get wrong: a custom property is
	   substituted on the element that declares it, so a `--sidebar-width` computed at :root would
	   read :root's `--sidebar-width-preference` and never see the one the shell writes on <body>.
	   Declared on .app-shell -- a descendant of <body> -- it sees the override, and .app-sidebar
	   inherits the answer rather than clamping a second time. */
	--sidebar-width: clamp( var( --sidebar-width-minimum ), var( --sidebar-width-preference ), var( --sidebar-width-maximum ) );

	display: grid;
	grid-template-columns: var( --sidebar-width ) minmax( 0, 1fr );
	grid-template-rows: minmax( 0, 1fr ) auto;
	grid-template-areas:
		"sidebar content"
		"footer  footer";
	height: 100vh;
	height: 100dvh;
}


/* ---------------------------------------------------------------------------------------------
   Sidebar.
   --------------------------------------------------------------------------------------------- */

.app-sidebar
{
	grid-area: sidebar;
	display: flex;
	flex-direction: column;
	min-height: 0;
	background: var( --sidebar-background );
	border-right: 1px solid var( --rule );
	color: var( --sidebar-ink );
	user-select: none;

	/* So the resize handle can sit on the right edge. Nothing else in here is positioned, and the
	   narrow-screen rule below replaces this with `fixed`, which is also a positioning context. */
	position: relative;
}


/* ---------------------------------------------------------------------------------------------
   The resize handle.

   **Built by sidebar.js rather than rendered by the server, and that is the point of it.** Every
   other control in this sidebar is a link or a form that works with no client script; a drag is not
   one of those and cannot be made into one. A server-rendered handle would therefore be a control
   that is visibly there and does nothing for anybody whose JavaScript failed to arrive -- worse than
   absent, because absent is honest. So the markup appears only where the behaviour does.

   Seven pixels wide and straddling the border, which is three more than looks necessary and is the
   difference between a handle you grab and a handle you hunt for. It draws nothing until it is
   hovered or focused: a permanent grab bar on the edge of the navigation would be a line competing
   with the border that is already there.
   --------------------------------------------------------------------------------------------- */

.sidebar-resizer
{
	position: absolute;
	top: 0;
	right: -3px;
	bottom: 0;
	width: 7px;
	z-index: 5;
	padding: 0;
	border: 0;
	background: none;
	cursor: col-resize;

	/* Without this a touch drag scrolls the page instead of moving the handle -- the pointer events
	   arrive either way, but the browser has already claimed the gesture by the time they do. */
	touch-action: none;
}

.sidebar-resizer::before
{
	content: "";
	position: absolute;
	inset: 0 3px;
	background: var( --selection-accent );
	opacity: 0;
	transition: opacity .12s ease;
}

.sidebar-resizer:hover::before,
.sidebar-resizer:focus-visible::before,
body[ data-sidebar-resizing="true" ] .sidebar-resizer::before
{
	opacity: .7;
}

/* The cursor and the selection lock belong to the whole document while a drag is in progress, not
   to the handle: the pointer is captured, so it spends most of the drag somewhere over the content,
   and without this it picks up that region's cursor and starts selecting its text. */
body[ data-sidebar-resizing="true" ]
{
	cursor: col-resize;
	user-select: none;
}

.sidebar-header
{
	display: flex;
	align-items: center;
	gap: .5rem;
	/* Jon, 2026-09-03, folded in from experiments.css: the logo is a picture with its own internal
	   left margin, so .9rem of padding put it visibly further right than the sidebar rows beneath it.
	   Optical alignment, which is why the number is not the same as its neighbours'. */
	padding: .85rem 0 .6rem 1.1rem;
	/* The containing block for the logo's light switch, placed against the logo it sits beside --
	   and a size container, so the switch can measure the header's WIDTH in cqw on both axes. A
	   percentage cannot do that: in `top` it is a share of the height. The header's width comes
	   from the sidebar, never from what is in it, so inline-size containment changes nothing. */
	position: relative;
	container-type: inline-size;
}

/* The logo fills the sidebar's usable width -- 16.5rem less .9rem of padding each side, about
   235 px -- and keeps its own aspect from the width/height attributes, so nothing reflows when it
   loads. The source is 915 px wide, so it lands at roughly 3.9x and stays crisp on any display.
   max-width in px rather than 100% alone so it never scales UP past its native size on a wider
   sidebar, which is how a logo goes soft. */
.sidebar-logo
{
	display: block;
	width: 100%;
	max-width: 915px;
	height: auto;
}

/* Swapped by the theme the reader already has. Both files are in the markup so neither needs a
   round trip when the theme changes; only one is ever displayed.

   Which one is a TOKEN rather than a media query (D351). It was a media query, and that made the
   logo the one part of the sidebar a theme could not reach: a theme that is dark in the light
   scheme got the dark ground it asked for and the LIGHT logo on top of it, because the swap was
   keyed to the OS and a theme has no way to speak to the OS. The defaults are unchanged and the
   dark block below still swaps them, so nothing moves for the two themes that follow the scheme. */
.sidebar-logo-light { display: var( --logo-light-display ); }
.sidebar-logo-dark { display: var( --logo-dark-display ); }

/* The neon layer, laid exactly over whichever logo is showing: the same 915x172 source, the same
   width rule, pinned to the wordmark's padding box. Decorative (empty alt), and it takes no clicks,
   so the link underneath is still the thing you press. It is a separate image rather than a
   background so later colour, gradient and lighting effects can target it on its own --
   mix-blend-mode, filter, a mask -- without touching the logo files. */
.sidebar-wordmark { position: relative; }

.sidebar-logo-neon
{
	position: absolute;
	top: .15rem;
	left: .35rem;
	width: calc( 100% - .7rem );
	max-width: 915px;
	height: auto;
	pointer-events: none;
	filter: var( --logo-neon-filter );
}

/* A colour layer cut to the neon image's own shape. The PNG is the MASK, not the paint: whatever
   --logo-neon-overlay paints -- a flat colour, a gradient, several stacked -- shows only where the
   neon has opaque pixels, and its soft glow edges carry through as partial alpha. Same box as the
   image above, so the two register exactly; ::after of the link paints after the image, so it sits
   on top of it. --logo-neon-overlay-blend decides how it meets the neon underneath: `normal`
   replaces its colour, `color` or `hue` recolours it and keeps its light, `screen` adds light.
   A pseudo-element rather than a second <img>, so the markup stays one image. */
.sidebar-wordmark::after
{
	content: "";
	position: absolute;
	top: .15rem;
	left: .35rem;
	width: calc( 100% - .7rem );
	max-width: 915px;
	aspect-ratio: 915 / 172;
	pointer-events: none;
	background: var( --logo-neon-overlay );
	/* Twice the box, slid by exactly one tile width per pass: the colours drift through the letters
	   and the loop has no jump. Anything that is not a gradient simply ignores the size. */
	background-size: 200% 100%;
	mix-blend-mode: var( --logo-neon-overlay-blend );
	-webkit-mask: url( "/assets/buffet_neon.png" ) center / 100% 100% no-repeat;
	mask: url( "/assets/buffet_neon.png" ) center / 100% 100% no-repeat;
}

/* The light is OFF unless the account says on (<body data-logo-light>, drawn by the server): the
   neon keeps its own look and the rainbow is an easter egg behind the switch. `:not(on)` rather
   than `[off]`, so a page that draws no attribute at all gets the default. Hover still lights it
   turquoise -- that is how the switch is found. */
body:not([data-logo-light="on"]) { --logo-neon-overlay: none; }

/* The drift runs ONLY while the light is on (D644). It animates background-position through a mask
   and a blend mode, which no compositor can take over: every frame is a style pass and a repaint of
   that layer. It used to run with the light off too -- sliding an overlay of `none` sixty times a
   second on every page, which Jon's inspector showed as continuous rendering on an idle page. */
body[data-logo-light="on"] .sidebar-wordmark::after
{
	animation: logo-neon-drift var( --logo-neon-drift-duration ) linear infinite;
}

/* THE LOGO'S LIGHT SWITCH, beside the last letter. Placed in pure CSS against the logo's width,
   which is the header's content width (cqw already leaves out its left padding) less the link's own
   padding (.7rem), up to the image's native 915px. Two measured numbers from buffet_neon.png's opaque pixels: the "T"
   ends at 817 of 915 (.893 of the width), and the letters' middle is 87 of 172 down -- .0951 of the
   WIDTH, since the height follows it. So the switch stays by the T however wide the sidebar is
   dragged. It sits a little clear of the T, on the letters' middle line. Hidden until the header is hovered or the switch has keyboard focus; a touch screen has
   no hover, so there it stays faintly visible. */
.sidebar-logo-switch
{
	--logo-switch-logo-width: min( 100cqw - .7rem, 915px );
	position: absolute;
	left: calc( 1.45rem + .893 * var( --logo-switch-logo-width ) + .5rem );
	top: calc( 1rem + .0951 * var( --logo-switch-logo-width ) );
	transform: translateY( -50% );
	display: grid;
	place-items: center;
	width: 1.1rem;
	height: 1.1rem;
	padding: 0;
	border: 0;
	border-radius: .3rem;
	background: none;
	color: var( --ink-quiet );
	cursor: pointer;
	opacity: 0;
	transition: opacity .15s ease;
}

.sidebar-header:hover .sidebar-logo-switch,
.sidebar-logo-switch:focus-visible
{
	opacity: 1;
}

@media ( hover: none )
{
	.sidebar-logo-switch { opacity: .5; }
}

.sidebar-logo-switch:hover { color: var( --ink ); }

.sidebar-logo-switch svg
{
	width: 100%;
	height: 100%;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.25;
}

/* The rocker: its lit half up and turquoise while the light is on, down and plain when it is off
   (the default, so the down position is the rule's and `on` lifts it). */
.sidebar-logo-switch .switch-lever
{
	fill: currentColor;
	stroke: none;
	transform: translateY( 4px );
	transition: transform .15s ease, fill .15s ease;
}

body[data-logo-light="on"] .sidebar-logo-switch .switch-lever
{
	fill: var( --logo-neon-hover-overlay );
	transform: none;
}

/* With a background twice the box's width, 200% is exactly one tile, so the last frame IS the
   first. */
@keyframes logo-neon-drift
{
	from { background-position: 0% 0; }
	to   { background-position: 200% 0; }
}

/* A person who asked for less motion gets the gradient standing still. */
@media ( prefers-reduced-motion: reduce )
{
	.sidebar-wordmark::after { animation: none; }
}

.sidebar-wordmark
{
	display: flex;
	align-items: center;
	gap: .5rem;
	font-size: var( --text-lg );
	font-weight: 650;
	letter-spacing: -.01em;
	color: var( --ink );
	padding: .15rem .35rem;
	border-radius: .4rem;
}

.sidebar-wordmark:hover
{
	text-decoration: none;
}

/* The logo row lights up instead of taking the standard row-hover background. The colour goes
   through the mask on ::after; the glow goes on the neon <img>, because a filter is applied BEFORE
   the mask and a drop-shadow on the masked layer would be cut away with the rest of it. Both
   filters are listed so the dark scheme's --logo-neon-filter still applies under the glow. */
.sidebar-wordmark:hover::after
{
	background: var( --logo-neon-hover-overlay );
}

.sidebar-wordmark:hover .sidebar-logo-neon
{
	filter: var( --logo-neon-filter ) var( --logo-neon-hover-glow );
}

.sidebar-wordmark .mark
{
	display: grid;
	place-items: center;
	width: 1.35rem;
	height: 1.35rem;
	border-radius: .35rem;
	background: var( --accent );
	color: #fff;
	font-size: var( --text-sm );
	font-weight: 700;
}

/* The search field. Filters the sidebar as you type and searches items on Enter -- two behaviours
   in one control, which is how Finder's search field works and is worth copying because the
   ambiguity is resolved by the key you press rather than by a mode you have to remember. */
.sidebar-search
{
	/* The containing block for the key hint drawn inside the field's right edge. */
	position: relative;
	padding: 0 .9rem .55rem;
}

.sidebar-search input
{
	width: 100%;
	/* Right padding clears the "⏎ to search" hint and the clear button beside it, so typed text
	   never runs underneath either. */
	padding: .35rem 6.6rem .35rem 1.75rem;
	border: 1px solid var( --rule );
	border-radius: .45rem;
	background: var( --card-background ) url( "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235c626c' stroke-width='1.5'><circle cx='6.75' cy='6.75' r='4.25'/><path d='M10 10l3.5 3.5'/></svg>" ) no-repeat .45rem 50% / .9rem;
	color: var( --ink );
	font: inherit;
}

.sidebar-search input::placeholder
{
	color: var( --ink-quiet );
}

/* WebKit's own cancel button draws at the right edge of the CONTENT box, which the padding above
   pulls inward to the left of the key hint. Jon wants the clear control at the field's edge, so the
   native one is switched off and `.search-clear` below stands in for it -- in Firefox too, which
   never drew one. */
.sidebar-search input::-webkit-search-cancel-button
{
	-webkit-appearance: none;
	appearance: none;
	display: none;
}

.sidebar-search .search-clear
{
	position: absolute;
	top: 50%;
	right: calc( .9rem + .3rem );
	transform: translateY( -50% );
	margin-top: -.275rem; /* the same correction the key hint makes for .sidebar-search's bottom padding */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* Four fifths of the small round button (D430), as a keyword's × is: it sits inside a field. */
	width: calc( var( --icon-button-size ) * .8 );
	height: calc( var( --icon-button-size ) * .8 );
	padding: 0;
	border: 0;
	border-radius: .3rem;
	background: transparent;
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-lg );
	line-height: 1;
	cursor: pointer;
}

.sidebar-search .search-clear:hover,
.sidebar-search .search-clear:focus-visible
{
	color: var( --ink );
	background: var( --rule );
}

/* Nothing to clear, nothing to show. The placeholder is visible exactly when the field is empty, and
   the button is a later sibling of the input, so the stylesheet can decide this without script. */
.sidebar-search input:placeholder-shown ~ .search-clear
{
	display: none;
}

/* The hint that the second behaviour exists. Typing filters, which is visible as it happens; Enter
   searches, which nothing on screen said until this. Drawn inside the field at its right edge and
   inert to the pointer, so a click on it lands in the input. `aria-hidden` in the markup: the
   field's label already says "or press Enter to search items", and a screen reader does not need a
   second, shorter copy of the same instruction. */
.sidebar-search .search-key-hint
{
	position: absolute;
	top: 50%;
	/* Beside the clear button's slot, whether or not the button is showing -- computed from the
	   button's own size (D430), because a stated 1.8rem was true of a 1.3rem × and crowded a 2rem one. */
	right: calc( .9rem + .3rem + var( --icon-button-size ) * .8 + .25rem );
	transform: translateY( -50% );
	margin-top: -.275rem; /* half of .sidebar-search's bottom padding, so the hint centres on the input rather than on the box */
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	line-height: 1;
	white-space: nowrap;
	pointer-events: none;
}

.sidebar-search .search-key-hint kbd
{
	display: inline-block;
	min-width: 1.1em;
	padding: .1rem .3rem;
	border: 1px solid var( --rule );
	border-radius: .3rem;
	background: var( --page-background, transparent );
	color: inherit;
	font: inherit;
	font-size: var( --text-sm );
	text-align: center;
}

.sidebar-scroll
{
	/* The containing block for the reorder slot drawn between two rows (`.mover-slot`): the scroll
	   region rather than a list, so one element serves every list in the tree and scrolls with it. */
	position: relative;
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	padding: 0 .55rem 1rem;
	overscroll-behavior: contain;
}

.sidebar-section
{
	margin: 0 0 .55rem;
}

.sidebar-section-heading
{
	display: flex;
	align-items: center;
	gap: .3rem;
	width: 100%;
	padding: .3rem .35rem .25rem;
	border: 0;
	background: none;
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-xs );
	font-weight: 700;
	letter-spacing: .06em;
	text-transform: uppercase;
	text-align: left;
	cursor: default;
}

/* Drawn icons (html.php's sprite). One size variable so a row, a tile and a badge scale together
   rather than each carrying its own number -- and `currentColor` everywhere, which is what lets the
   selected row tint its icon with one rule instead of a second icon. */
.icon
{
	width: var( --icon-size, 1rem );
	height: var( --icon-size, 1rem );
	flex: 0 0 auto;
	display: block;
	color: inherit;
	stroke: currentColor;
	fill: none;
	vertical-align: middle;
}

/* The disclosure control. It was a .6rem text triangle -- legible, but a target the size of a full
   stop, and the first thing Jon called out. Now a drawn chevron at nearly twice that, with the hit
   area padded out past the ink so the whole corner of the heading is clickable. */
.sidebar-disclosure
{
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: .2rem;
	margin-left: -.2rem;

	/* Merged from a SECOND .sidebar-disclosure block that sat two hundred lines below this one and,
	   being later, quietly won: its `padding: 0` cancelled the padded hit area the comment above
	   promises, so the control had been a bare-ink target for as long as both existed. Two
	   definitions of one thing, which is the arrangement D129 named and this file keeps finding the
	   wrong end of. */
	border: 0;
	background: none;
	color: var( --ink-quiet );
	line-height: 1;
	cursor: pointer;
}

/* ---------------------------------------------------------------------------------------------
   The heading button holds the chevron AND the name now (D174), so it has to size to that.

   [TRAP] It did not, and the failure did not look like a layout bug. This rule was written when the
   button held a 1rem icon and nothing else, so it carried no `flex` and no `white-space`. As a flex
   item that may shrink, with a word inside, the browser squeezed it to its narrowest fit and broke
   the label to one letter per line -- a column of capitals, which reads exactly as though the whole
   heading had been rotated 90 degrees. Jon reported it as "tilted in full, including the word", and
   he was describing a wrap.

   `font: inherit` matters as much as the width: .sidebar-tool sets its own font-size, and the name
   only belongs to the heading's typography while it inherits it.
   --------------------------------------------------------------------------------------------- */

.sidebar-section-heading .sidebar-disclosure
{
	flex: 1 1 auto;
	min-width: 0;
	justify-content: flex-start;
	gap: .3rem;
	white-space: nowrap;
	text-align: left;

	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	color: inherit;
}

.sidebar-section-name
{
	overflow: hidden;
	text-overflow: ellipsis;
}

.sidebar-section-heading .disclosure
{
	--icon-size: 1.05rem;
	color: var( --ink-quiet );
	transition: transform .14s ease;
}

.sidebar-section-heading:hover .disclosure
{
	color: var( --ink );
}

/* A collapsed section hides its BODY, and the empty-state paragraph is part of the body.
   It used to name .sidebar-list alone, which meant a section holding nothing but a hint could not be
   put away: "Nothing pinned yet. Drag a project here" sat in the sidebar permanently, for exactly
   the person who had already read it and collapsed the section to make it stop. Same for "You are
   not a member of any team" and for the sentence an actor with no durable identity gets under
   Favorites -- the three places where a hint is the only content there is. */
.sidebar-section[data-collapsed="true"] .sidebar-list,
.sidebar-section[data-collapsed="true"] .sidebar-empty
{
	display: none;
}

/* ---------------------------------------------------------------------------------------------
   Collapsible branches: a team or a project that contains rows opens and closes by its own name.

   The whole heading is the section's click target now, and the same idea one level down -- a 12px
   triangle beside an inert word asks a person to hit the smallest thing on the row.

   `> ul` and not a descendant selector: closing a team must hide the projects under it and let each
   of THOSE keep its own open state, so reopening the team restores the shape it had rather than
   flattening everything inside it.
   --------------------------------------------------------------------------------------------- */

.sidebar-list li[data-collapsible="true"][data-collapsed="true"] > .sidebar-list
{
	display: none;
}

/* The slot. Sized the same whether or not it holds a chevron, because that is what keeps the labels
   in a column -- index.php renders the empty one deliberately and app.css must not undo it. */
.row-disclosure
{
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	width: 1rem;
	margin-left: -.3rem;
	border-radius: .25rem;
	color: var( --ink-quiet );
	transition: transform 120ms ease;
}

/* ---------------------------------------------------------------------------------------------
   D234 made this the ONLY thing that closes a branch -- the row's name no longer folds it -- and a
   control that is the only route to an action has to be reachable. It was a .62rem chevron in a
   .7rem slot: a target about four millimetres square, which was acceptable while it was the *second*
   way to do something and is not now.

   `align-self: stretch` rather than padding, so the hit area is the full height of the row without
   the row growing to accommodate it. The slot widens by .35rem and pulls .15rem of that back out of
   the indent, so a deep tree loses almost nothing.
   --------------------------------------------------------------------------------------------- */

.row-disclosure:not( [data-empty] )
{
	align-self: stretch;
	cursor: pointer;
}

.row-disclosure:not( [data-empty] ):hover
{
	color: var( --ink );
	background: color-mix( in srgb, var( --ink ) 12%, transparent );
}

.row-disclosure .icon { --icon-size: .62rem; }

/* The ICON, not its slot -- the same lesson as the section chevron above, applied before it can
   bite. The slot is currently a span containing only the chevron, so rotating either would look
   identical today; it stops looking identical the first time anything else goes in there. */
li[data-collapsed="true"] > .sidebar-row .row-disclosure .icon
{
	transform: rotate( -90deg );
}

@media ( prefers-reduced-motion: reduce )
{
	.row-disclosure { transition: none; }
}

/* ---------------------------------------------------------------------------------------------
   ON A PHONE, AND UNDER ANY FINGER, THE SLOT IS A THUMB'S WIDTH (Jon, 2026-09-11).

   In the sheet the 1rem slot above was about 16 px of target beside a link that spans the rest of
   the row, so a fold aimed a few pixels wide opened the page instead -- the costlier of the two
   mistakes, since a fold is undone with one more tap and a navigation is not. Two things change:

   · the slot is 2.5rem wide (~40 px, near the platform minimum), the row a little taller, and the
     chevron a little larger;
   · the chevron sits at the LEFT of its slot rather than the centre, so the extra width lies between
     the chevron and the label -- a safety margin in the one direction a miss goes. A tap there folds
     rather than navigates, because the slot is the click handler's target (app.js, `closest(
     '.row-disclosure' )`) and nothing about the handler had to change. The .4rem gap beyond the slot
     still belongs to the link, and so does the glyph -- that is the part of the row that IS the link.

   The empty slot on a childless row widens by the same amount, which is what keeps the labels in a
   column (the rule the slot's first comment states). The indent per depth is trimmed slightly so a
   deep folder still fits on a narrow sheet.

   Both media features, not one: the sheet breakpoint catches a phone, and `pointer: coarse` catches
   a tablet wide enough to keep the sidebar column but still driven by a finger. A laptop with a
   touch screen reports its finest pointer (the trackpad) and keeps the desktop slot.
   --------------------------------------------------------------------------------------------- */

@media ( max-width: 46rem ), ( pointer: coarse )
{
	.row-disclosure
	{
		box-sizing: border-box;
		width: 2.5rem;
		margin-left: -.45rem;
		justify-content: flex-start;
		padding-left: .5rem;
	}

	.row-disclosure .icon { --icon-size: .8rem; }

	/* A pressed state a finger can see, since there is no hover under one. */
	.row-disclosure:not( [data-empty] ):active
	{
		color: var( --ink );
		background: color-mix( in srgb, var( --ink ) 18%, transparent );
	}

	.sidebar-row
	{
		padding-top: .42rem;
		padding-bottom: .42rem;
		padding-left: calc( .4rem + var( --depth, 0 ) * .7rem );
	}
}

.sidebar-section-tools
{
	margin-left: auto;
	display: flex;
	gap: .1rem;
}

.sidebar-tool
{
	border: 0;
	background: none;
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-sm );
	line-height: 1;
	padding: .15rem .25rem;
	border-radius: .3rem;
	cursor: pointer;
}

.sidebar-tool:hover
{
	background: var( --selection-background );
	color: var( --ink );
}

.sidebar-tool[aria-pressed="true"]
{
	color: var( --selection-accent );
}

.sidebar-list
{
	list-style: none;
	margin: 0;
	padding: 0;
}

/* The sidebar filter (sidebar.js applySidebarFilter) hides a non-matching row by setting `hidden`
   on its <li>, and two rules further down silently undid that: `.row-with-control` and
   `.favorite-item` are both `display: flex`, and an author `display` beats the user-agent's
   `[hidden] { display: none }` regardless of specificity -- the same trap the footer's status groups
   fell into. Every row that carries a pin control, and every favorite, became unfilterable when
   those rules landed at D222, and nothing noticed because the proof suite runs no client JavaScript
   (Blueprint §19.2). One rule over the whole scroll region rather than one per row class, so the
   next row class that takes a `display` of its own cannot break the filter again. */
/* D431: the filter no longer sets `hidden` -- sidebar.js marks a failed row `data-filtered-out` and
   this file decides what that looks like, so the paragraph above is history and its last sentence
   is still the rule. WIDE: gone, as it always was. The `not all and` form is the one the breadcrumb
   block uses, so the two halves cannot both match. */
@media not all and ( max-width: 46rem )
{
	.sidebar-scroll li[data-filtered-out]
	{
		display: none;
	}
}

/* NARROW: kept and dimmed (Jon, 2026-09-18). The sheet is pinned to the bottom of the screen and is
   as tall as its rows, with the filter field at its top; removing rows lowered the field under the
   person's typing and could drop the whole sheet behind the keyboard. A dimmed row moves nothing.
   The second rule is arithmetic: opacity multiplies down a subtree, and a filtered-out branch holds
   only filtered-out rows, so without it a grandchild would be drawn at .3 x .3 x .3. */
@media ( max-width: 46rem )
{
	.sidebar-scroll li[data-filtered-out] { opacity: .3; }
	.sidebar-scroll li[data-filtered-out] li[data-filtered-out] { opacity: 1; }
}

/* One row. The whole row is the hit target, the indent is a CSS variable set per depth, and the
   selected row is a filled pill rather than a highlighted line -- Photos' idiom, and it survives
   a nested tree far better than a full-bleed bar. */
.sidebar-row
{
	display: flex;
	align-items: center;
	gap: .4rem;
	padding: .28rem .4rem;
	padding-left: calc( .4rem + var( --depth, 0 ) * .85rem );
	border-radius: .4rem;
	color: var( --sidebar-ink );
	font-size: var( --text-base );
	cursor: default;
	white-space: nowrap;
}

.sidebar-row:hover
{
	text-decoration: none;
	background: color-mix( in srgb, var( --selection-background ) 55%, transparent );
}

.sidebar-row[aria-current="page"]
{
	background: var( --selection-background );
	font-weight: 600;
}

.sidebar-row .row-glyph
{
	--icon-size: 1.05rem;
	flex: 0 0 1.15rem;
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
	color: var( --ink-quiet );
	font-size: var( --text-md );
}

/* A folder is the shape this tree is mostly made of, so it carries slightly more ink than the
   scopes above it -- a hexagon and a divided panel read heavier at the same stroke width. */
.sidebar-row .row-glyph[data-icon="folder"],
.sidebar-row .row-glyph[data-icon="folder-open"]
{
	--icon-size: 1.15rem;
}

/* A team is the top of the tree and gets the accent even when it is not the current row: it is the
   one scope whose identity is the tenant boundary, and the tree is easier to read down from it. */
.sidebar-row .row-glyph[data-icon="team"]
{
	color: color-mix( in srgb, var( --selection-accent ) 70%, var( --ink-quiet ) );
}

.sidebar-row[aria-current="page"] .row-glyph
{
	color: var( --selection-accent );
}

.sidebar-row .row-label
{
	flex: 1;
	overflow: hidden;
	text-overflow: ellipsis;
}

.sidebar-row .row-count
{
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	font-variant-numeric: tabular-nums;
}

/* A scope that is only visible because something inside it is: drawn, but drawn as a path rather
   than as a place. The distinction has to be visible or the tree lies about what you can open. */
.sidebar-row[data-visibility="path"] .row-label
{
	color: var( --ink-quiet );
	font-style: italic;
}

/* ARCHIVED (D271). Muted, never hidden -- which is the whole feature in one declaration.
   
   The row keeps its icon, its label, its count and every control beside it; it is simply quieter
   than the live work above it, which is what "demote" means in a column you cannot sort. Opacity
   rather than a colour, so it applies to the icon, the count and the pin in one rule and cannot
   drift out of step with whichever of them somebody restyles next.
   
   The permanent Archive row of an EMPTY team is drawn with this same attribute, deliberately: an
   archive nobody has used yet should read as a door rather than as a destination, and the row still
   has to be there to teach that the door exists. */
.sidebar-row[data-archived="true"]
{
	opacity: .58;
}

.sidebar-row[data-archived="true"]:hover,
.sidebar-row[data-archived="true"][aria-current="page"]
{
	opacity: 1;
}

.sidebar-row .row-share
{
	color: var( --selection-accent );
	font-size: var( --text-xs );
}

/* ---------------------------------------------------------------------------------------------
   The chevron turns. The button does not.

   [TRAP] This was `.sidebar-disclosure[aria-expanded="false"]`, which is the BUTTON -- harmless for
   as long as the button contained nothing but a chevron, and the moment D174 put the section's name
   inside it, the whole heading turned on its side, word and all. Jon reported it precisely: "the
   button element gets the rotate(90deg) and the span with the text is inside it".

   The rule was not touched by the change that broke it, which is the second time in two days: D170's
   `.dropzone-active` was scoped to `#dropzone` and stopped styling anything when a second drop zone
   appeared. **A rule written for one content shape, inherited by another** -- invisible in review,
   because the diff and the defect are in different files.

   `aria-expanded` on the button is the signal, and `.disclosure` inside it is the thing that moves.
   --------------------------------------------------------------------------------------------- */

.sidebar-disclosure[aria-expanded="false"] .disclosure
{
	transform: rotate( -90deg );
}

/* The drag vocabulary -- the fade, the "into" fill, the refusal, the received flash, and the
   reorder slot between two rows -- is the MOVING THINGS block further down, shared with the grid
   (D300). The sidebar has no rule of its own any more: its caret used to be an inset shadow here,
   which read as a rounded border on the wrong row, and is now the same `.mover-slot` element the
   grid draws between two cards, placed by sidebar.js inside `.sidebar-scroll`. */

.sidebar-empty
{
	padding: .2rem .5rem .5rem;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
	font-style: italic;
}

/* THE GUIDE AND SIGN OUT, one centred row under the wordmark (Jon, 2026-09-17; index.php says why
   they left their box at the foot of the sidebar). Rows in the same list shape as the tree, laid on
   their side: each keeps its hover fill and, for the guide, the current-row highlight, at the small
   print's size D381 gave them. The horizontal padding is .sidebar-scroll's, so the row centres on the
   same width the tree below it is drawn in. */
.sidebar-links
{
	padding: 0 .55rem .5rem;
}

.sidebar-links .sidebar-list
{
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: .1rem .35rem;
}

.sidebar-links .sidebar-row
{
	gap: .3rem;
	padding: .16rem .5rem;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

/* Sign out's button is full-width for the tree's sake (button.sidebar-row, below); in a row it is
   as wide as its words. And no shadow: a general `button` rule (experiments.css today) would draw it
   as a pill beside a guide link that is not one, and D267 is Jon's word that it reads as a row. */
.sidebar-links button.sidebar-row
{
	width: auto;
	box-shadow: none;
}

/* The tree's disclosure slot keeps labels in a column; in a row there is no column to keep. */
.sidebar-links .row-disclosure
{
	display: none;
}

.sidebar-links .row-glyph
{
	--icon-size: .9rem;
	flex-basis: auto;
}

/* A label that takes the free space is a tree row's rule; here it would push the two apart. */
.sidebar-links .sidebar-row .row-label
{
	flex: none;
}

.actor-signout
{
	margin: 0;
}

/* Standing in for somebody (D252), the one thing left at the foot of the sidebar. The rule and the
   gap above it are .actor-standin's own, below; this is only the width the tree is drawn in. */
.sidebar-standin
{
	padding: 0 .55rem .7rem;
}

.sidebar-standin select
{
	/* Chrome, not content: keeps the OS arrow, because the `background:` shorthand below would
	   wipe the chevron the element rule draws (see `select` near the top of the sheet). */
	appearance: auto;
	-webkit-appearance: auto;
	width: 100%;
	padding: .3rem .4rem;
	border: 1px solid var( --rule );
	border-radius: .4rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-base );
}

/* **A `<button>` wearing .sidebar-row, because sign out must stay a POST** (D192, D267) and a link
   would have been the wrong element for it. A button brings its own font, its own chrome and its own
   centred text, none of which .sidebar-row thinks to override -- it was written when every row was
   an `<a>` and an `<a>` has none of them. So the reset lives here, next to the class it completes,
   rather than being folded into .sidebar-row where it would be four properties that mean nothing for
   the fifty-odd rows that are still links.

   The same reset .sidebar-section-heading already does for the same reason, twelve rules above. */
button.sidebar-row
{
	width: 100%;
	border: 0;
	background: none;
	font: inherit;
	font-size: var( --text-base );
	text-align: left;
	cursor: default;
}


/* Standing in for somebody (D252). Two states in one class: the form that offers it, and the banner
   that says one is in force. */
.actor-standin
{
	margin-top: .55rem;
	padding-top: .55rem;
	border-top: 1px dashed var( --rule );
}

.actor-standin-row
{
	display: flex;
	align-items: center;
	gap: .4rem;
}

/* The select takes the space and the button takes what it needs. Without this the button wraps its
   own word at the sidebar's width, which is narrow by design. */
.actor-standin-row select
{
	flex: 1 1 auto;
	min-width: 0;
}

.actor-standin-row .control-pill
{
	flex: 0 0 auto;
}

.actor-standin-note
{
	margin: .35rem 0 0;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	line-height: 1.35;
}

/* THE BANNER IS NOT DECORATION, and the colour is the argument. An interface quietly showing you
   somebody else's library is one in which every later surprise -- the missing team, the absent
   upload button, the folder that will not open -- looks like a defect until you remember. So it
   takes the warning tone the rest of the shell reserves for a state a person must not miss, and it
   sits above the way back rather than beside it. */
[data-standing-in="yes"]
{
	margin-top: .55rem;
	padding: .5rem .55rem .55rem;
	border: 1px solid color-mix( in srgb, var( --warning ) 45%, var( --rule ) );
	border-radius: .45rem;
	background: var( --warning-quiet );
}

.actor-standin-banner
{
	margin: 0;
	font-size: var( --text-sm );
	font-weight: 600;
	line-height: 1.35;
}

.actor-standin-detail
{
	margin: .25rem 0 .45rem;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	line-height: 1.35;
}


/* ---------------------------------------------------------------------------------------------
   Content column: toolbar + scrolling main.
   --------------------------------------------------------------------------------------------- */

.app-content
{
	grid-area: content;
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;

	/* What the toolbar lies over (below): it is taken out of the flow and pinned to this box's top. */
	position: relative;

	/* How far the toolbar reaches down over .app-main, which pads its top by this so the first row
	   starts clear of it. The toolbar is a fixed band (no wrapping; its tallest control is sized to
	   --toolbar-height), so its min-height is its height and one number serves both. */
	--toolbar-overlap: calc( var( --toolbar-height ) + var( --top-clearance, 0px ) );
}

.app-toolbar
{
	display: flex;
	align-items: center;
	gap: .75rem;
	min-height: var( --toolbar-height );
	/* Horizontal only, matching .app-main's gutter so the breadcrumb lines up with the grid beneath
	   it. The vertical stays 0: this toolbar takes its height from its own contents. */
	padding: 0 1rem;

	/* GLASS OVER THE CONTENT (Jon, 2026-10-02, minimalism pass). The content's own ground at 0.9 with
	   a blur, and no rule beneath: the band separates by the content passing under it rather than by
	   a line. That only works if there IS content under it, so the toolbar lies over the top of
	   .app-content instead of stacking above .app-main, and .app-main pads its top by
	   --toolbar-overlap. Every theme, Riviera included (its water pane is the footer's alone now).

	   The filter makes this band the containing block for `fixed` descendants. Nothing here relies on
	   escaping it (shell/toolbar.php's renderToolbar moved out the floater it once trapped), and the photo
	   wall, whose .grid-controls is fixed and lives here, turns the filter off. Being absolutely
	   positioned also keeps this the box the expanded breadcrumb measures from. */
	background: color-mix( in srgb, var( --page-background ) 90%, transparent );
	-webkit-backdrop-filter: saturate( 180% ) blur( 12px );
	backdrop-filter: saturate( 180% ) blur( 12px );
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 3;

	/* What the controls ask how much room they have (the `@container toolbar` block, which says why
	   it is not the viewport). Safe as inline-size containment: this band's width comes from its
	   left/right pins, never from what is in it. */
	container: toolbar / inline-size;
}

/* THE SQUARE MARK (D353, Jon 2026-09-07). Off at every width but the narrow one, and the narrow block
   far below is what turns it on -- so a wide screen, where the sidebar's wordmark is already on
   screen, never draws a second logo saying the same thing. `line-height: 0` because the anchor is
   otherwise a text box a couple of pixels taller than the picture inside it, which would push the
   toolbar band open by that much. */
.toolbar-mark
{
	display: none;
	flex: 0 0 auto;
	line-height: 0;
}

/* **The box is not the mark, and that is why this number is not 1.8rem.** The source is a 172 px
   square whose ink occupies 138 px of it -- 18 px of transparency above, 16 below -- so the picture
   you see is 80% of whatever the box is set to. At 2.55rem (40.8 px) the ink lands at about 33 px
   against the 1.8rem (28.8 px) controls beside it: two pixels proud at the top and two at the
   bottom, which is what Jon asked for and what a mark needs to read as the thing the row hangs off
   rather than as one more control in it. The band is --toolbar-height, 3.25rem, so 40.8 px still
   leaves ~5 px of air each side and the toolbar does not grow.

   Sized in one direction only would be wrong here: both dimensions are stated so nothing reflows
   when the image lands, and the source is square so the aspect is preserved by arithmetic. The
   ink's own left margin, 18 px of the 172, scales to ~4 px and adds to the toolbar's .75rem gutter
   to put the mark's left edge on the same ~16 px as the heading below it -- optical alignment, got
   by measurement rather than by luck, so do not "fix" the padding without re-measuring. */
.toolbar-mark-image
{
	width: 2.55rem;
	height: 2.55rem;
}

/* The same two tokens the sidebar's pair reads, for the same reason (D351): a theme can set a token
   and cannot speak to the OS, so a swap keyed to `prefers-color-scheme` is how a theme that is dark
   in the light scheme ends up with the light mark on its dark ground. */
.toolbar-mark-light { display: var( --logo-light-display ); }
.toolbar-mark-dark { display: var( --logo-dark-display ); }

/* The breadcrumb IS the containment chain (viewstate.php builds it from the same array the access
   check walks). It is not decoration and not a history trail -- it is where you are. */
.breadcrumb
{
	display: flex;
	align-items: center;
	gap: .3rem;
	min-width: 0;
	margin: 0;
	padding: 0;
	list-style: none;
	font-size: var( --text-base );
}

.breadcrumb li
{
	display: flex;
	align-items: center;
	gap: .3rem;
	min-width: 0;
}

.breadcrumb li + li::before
{
	content: "›";
	color: var( --ink-quiet );
}

.breadcrumb a
{
	color: var( --ink-quiet );
	padding: .15rem .3rem;
	border-radius: .3rem;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.breadcrumb a:hover
{
	text-decoration: none;
	background: var( --selection-background );
	color: var( --ink );
}

.breadcrumb li:last-child a
{
	color: var( --ink );
	font-weight: 600;
}

/* THE NEW WINDOW GLYPH (D406): green, after the last crumb, inside its <li> -- "this view, in a new
   window". Green because it creates something (the `create` tone of the pills) and does not act on
   the world; a glyph and not a pill because the breadcrumb is a line of text and a pill in it would
   read as a fourth crumb. */
.breadcrumb-new-window
{
	--icon-size: .95rem;
	display: inline-flex;
	align-items: center;
	margin-left: .1rem;
	padding: .15rem .3rem;
	border: 0;
	border-radius: .3rem;
	background: none;
	color: var( --create );
	cursor: pointer;
}

.breadcrumb-new-window:hover { background: color-mix( in srgb, var( --create ) 14%, transparent ); }
.breadcrumb-new-window:focus-visible { outline: 2px solid var( --accent ); outline-offset: 1px; }

/* COPY ADDRESS (Jon, 2026-10-02), just before it and drawn the same way, in the quiet ink: it copies
   rather than creates, so it does not take the green. */
.breadcrumb-copy-address
{
	--icon-size: .95rem;
	display: inline-flex;
	align-items: center;
	margin-left: .1rem;
	padding: .15rem .3rem;
	border: 0;
	border-radius: .3rem;
	background: none;
	color: var( --ink-quiet );
	cursor: pointer;
}

.breadcrumb-copy-address:hover { background: color-mix( in srgb, var( --ink-quiet ) 14%, transparent ); color: var( --ink ); }
.breadcrumb-copy-address:focus-visible { outline: 2px solid var( --accent ); outline-offset: 1px; }
.breadcrumb-copy-address + .breadcrumb-new-window { margin-left: 0; }

/* The same glyph on an image's View chips, which open the picture in a window of its own. Green for
   the breadcrumb's reason; small, after the size, so the chip still reads as its size first. */
.rendition .rendition-new-window
{
	--icon-size: .8em;
	margin-left: .3em;
	color: var( --create );
	vertical-align: -.05em;
}

/* A PERSON'S OWN CONTAINER COLOUR (D592): the swatch row in the place's Color panel. Layout only, on
   Jon's instruction -- the seven `container-color-<name>` classes have no rules here at all. A row,
   a tile, a heading and a swatch all wear the same class, so one rule written for a name colours
   every one of them (experiments.css is where they are being tried). */
.container-color-choices
{
	display: flex;
	flex-wrap: wrap;
	gap: .4rem;
	margin: 0 0 1rem;
}

/* ---------------------------------------------------------------------------------------------
   A CLIPPED TRAIL OPENS ON HOVER (Jon, 2026-09-21).

   Every crumb above ellipsises on its own as the strip runs out of room, and a deep chain of
   long folder names can reach the point where each step reads `Client…` and the line no longer
   says where you are. Hovering it gives the names back.

   **Only when there is something to give back.** `data-crumbs-truncated` is written by app.js,
   which measures `scrollWidth > clientWidth` on each crumb -- the browser's own answer to "did the
   ellipsis appear", and a question no selector can ask. Without the attribute nothing here
   applies, so a trail that fits is a trail that does not move under the pointer.

   **It leaves the flex row rather than pushing it.** `position: absolute` is what keeps the
   selects and the Upload control still: `.toolbar-spacer` is `flex: 1`, so the width the trail
   gives up is taken by the spacer and nothing to its right shifts by a pixel. The alternative --
   letting it grow in flow -- would shove every control sideways at the moment somebody is aiming
   at one, which is §1.2's rule about not moving things under a cursor.

   It wraps instead of scrolling: a chain too long for one line becomes two or three, inside a card
   that overhangs the band. The card needs its own ground and its own shadow for that reason, and
   `z-index` because it hangs over the scrolling region below.

   `:focus-within` is not decoration -- tabbing along the crumbs with a keyboard is the same need,
   and without it the focused crumb would be the one nobody can read.
   --------------------------------------------------------------------------------------------- */

.app-toolbar .breadcrumb[data-crumbs-truncated="true"]:hover,
.app-toolbar .breadcrumb[data-crumbs-truncated="true"]:focus-within
{
	position: absolute;
	z-index: 30;
	top: .45rem;
	left: 1rem;
	max-width: calc( 100% - 2rem );
	flex-wrap: wrap;
	row-gap: .1rem;
	padding: .3rem .5rem;
	border: 1px solid var( --rule );
	border-radius: .5rem;
	background: var( --card-background );
	box-shadow: 0 8px 20px -10px rgba( 0, 0, 0, .5 );
}

.app-toolbar .breadcrumb[data-crumbs-truncated="true"]:hover a,
.app-toolbar .breadcrumb[data-crumbs-truncated="true"]:focus-within a
{
	overflow: visible;
	max-width: none;
	text-overflow: clip;
}

.toolbar-spacer
{
	flex: 1;
}

.toolbar-controls
{
	display: flex;
	align-items: center;
	gap: .4rem;
}

.toolbar-controls select,
.toolbar-controls button
{
	padding: .28rem .5rem;
	border: 1px solid var( --rule );
	border-radius: .42rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	cursor: pointer;

	/* Jon, 2026-09-03, folded in from experiments.css. Two selects and three buttons of five
	   different intrinsic widths read as five unrelated controls; one height and one minimum makes
	   them a row. The minimum is what carries the segmented layout control -- three buttons that
	   would otherwise be as wide as their words are long. */
	/* THE DECLARED EXCEPTION to --control-height, restated here rather than left to specificity: a
	   `min-height` clamps a `height`, so without this line the system rule would floor this strip at
	   2.35rem and the toolbar would grow past the band it lives in. The toolbar is a dense chrome
	   band sized to --toolbar-height; it is not the content vocabulary. */
	height: 1.8rem;
	min-height: 1.8rem;
	min-width: 4rem;
}

/* The wrapper index.php draws round each select (D430). On a wide screen it is not there: `display:
   contents` leaves the select a flex item of the strip exactly as before, and the glyph is not
   drawn. The narrow-screen block further down is where it becomes the control's visible half. */
.toolbar-select { display: contents; }
.toolbar-select-glyph { display: none; }

/* The same declared exception, for the arrow: this strip writes the `background:` shorthand, which
   would wipe the chevron the element rule draws, so its select keeps the OS appearance. */
.toolbar-controls select
{
	appearance: auto;
	-webkit-appearance: auto;
}

/* **THE KIND FILTER IS BOUNDED, because since D456 its selected value can be a KEYWORD** -- and a
   keyword is up to eighty characters (keywords.php). A family name is two syllables and never moved
   this control; one long word would push the breadcrumb along the strip every time somebody filtered
   by it. Capped here rather than by truncating the option text in PHP: an ellipsis written into the
   markup is also an ellipsis read out to a screen reader, where the browser's own clipping of the
   closed control costs the word only on screen. The OPEN list is the browser's own popup and is not
   clipped, so the full word is always available where it is being chosen. Harmless in the narrow-
   screen glyph mode below, where the select is laid over a ~2rem wrapper at width: 100%. */
.toolbar-controls select[data-view-param="kind"]
{
	max-width: 12rem;
}

.toolbar-controls button[disabled]
{
	opacity: .45;
	cursor: not-allowed;
}

/* **THE UPLOAD CONTROL** (D461), last in the strip and the only thing in this toolbar that writes.
   It is a <label> wrapping a hidden file input, or a <span> when this actor may not upload here, so
   neither is reached by the `select, button` rule above -- hence the height, restated rather than
   inherited, and it is the same declared exception to --control-height that rule states.

   `.toolbar-controls button[data-primary="true"]` used to sit here, styling an `Upload…` button that
   D201.3 deleted on 2026-08-29; nothing had rendered that attribute since, and it went with this
   edit (CLAUDE.md §3: a migration retires what it obsoletes). The accent now arrives the way every
   other pill's tone does, through `data-tone`, so there is one vocabulary for "this is the live one"
   instead of a rule that only this strip knew about. */
.toolbar-upload
{
	height: 1.8rem;
	min-height: 1.8rem;
	flex: 0 0 auto;
	white-space: nowrap;
}

/* A place this actor may not write to (ui-design.md §4.4): drawn, and drawn refused. The title
   carries the missing grant, and the element stays hoverable, because a tooltip nobody can reach is
   a refusal that does not say why. */
.toolbar-upload[data-upload-allowed="false"]
{
	opacity: .55;
	cursor: not-allowed;
}

/* The hasher's self-test failing is a BROKEN TOOL, not a refused person, and the two must not be
   drawn alike (D188, and the card's old comment made the same point): the uploader refuses every
   file while this sentence is on screen. `:empty` is the resting state -- the verdict comes from a
   test that has to actually run, so the server renders the element and says nothing in it. */
.toolbar-upload-limits:empty
{
	display: none;
}

.toolbar-upload-limits
{
	max-width: 16rem;
	color: var( --warning );
	font-size: var( --text-xs );
	font-weight: 600;
	line-height: 1.2;
}

/* The control itself once the self-test has failed. Deliberately NOT the refusal above: that is a
   permission with a named reason, this is a tool that does not work, and an interface drawing them
   the same way has collapsed two of Blueprint §2's three sentences into one grey pill. `aria-disabled`
   is written by upload.js on every `[data-upload-dropzone]`, which is this element -- and only ever
   on one whose permission verdict is yes, since the other kind carries no input to disable. */
.toolbar-upload[data-upload-allowed="true"][aria-disabled="true"]
{
	border-color: var( --warning );
	opacity: .75;
}

/* The loading strip. A two-second XHR needs to say so, and a strip at the top of the content is
   the least intrusive place to say it -- it does not move the layout and it does not replace what
   is already on screen with a spinner, which would be a step backwards from a page load. */
.load-strip
{
	/* On the toolbar's lower edge, over it: the toolbar lies over the content now (.app-toolbar), so
	   a strip left in the flow would be drawn beneath the glass. The photo wall has no band to sit
	   on, and its own rule below puts the strip at the top. */
	position: absolute;
	top: calc( var( --toolbar-overlap ) - 2px );
	left: 0;
	right: 0;
	z-index: 4;
	height: 2px;
	background: transparent;
	overflow: hidden;
}

.app-content[data-busy="true"] .load-strip::after
{
	content: "";
	position: absolute;
	inset: 0 auto 0 0;
	width: 35%;
	background: var( --accent );
	animation: load-sweep 1.1s ease-in-out infinite;
}

@keyframes load-sweep
{
	0%   { transform: translateX( -100% ); }
	100% { transform: translateX( 385% ); }
}

@media ( prefers-reduced-motion: reduce )
{
	.app-content[data-busy="true"] .load-strip::after
	{
		animation: none;
		width: 100%;
		opacity: .5;
	}
}

.app-main
{
	/* ONE COLUMN WIDTH FOR EVERYTHING IN HERE (Jon, 2026-08-30).

	   The dashboard now stacks three grids -- recently uploaded items, recently touched projects,
	   recently touched folders -- and they were laid out by two different templates: 15.5rem tracks
	   for the item grid and 14rem for the tiles. Two auto-fill grids of different track widths in
	   one column produce different numbers of columns at most window sizes, so the page read as
	   three grids that had been pasted together rather than as one page. Nothing lined up
	   vertically, and at some widths the tile grid gained a column the item grid did not, which is
	   the misalignment that reads as overlap.

	   So: one variable, read by .item-grid and .scope-grid alike, and every card and tile on a view
	   is the same width with its columns in the same places all the way down.

	   WIDTH IS FIXED; HEIGHT ABSORBS THE DIFFERENCE. A tile whose meta line wraps gets taller, and
	   .scope-grid > li plus equaliseScopeTiles() in app.js bring the rest of the rail up to match --
	   which is the right direction for the give to go, because a row of tiles at different WIDTHS
	   stops being a grid and a row at different heights is merely a taller row.

	   Scoped to .app-main rather than :root deliberately: the sidebar renders .scope-grid too, for
	   favourites, in a 16.5rem column that a 15.5rem minimum track would very nearly fill. The
	   grids out there keep the narrower fallback written into their own rule.

	   17.5rem rather than 15.5rem (Jon, 2026-09-22): "the cards tend to get a little too narrow
	   when resizing the viewport". auto-fill keeps a track at its minimum until one more fits, so
	   the minimum is what a card looks like at its worst, and 15.5rem was a card whose title
	   clamped at three words and whose footer wrapped on two pills -- the D473 card carries more on
	   its width than the one this number was set for. A wide window still packs as many as fit. */
	--card-track: 17.5rem;

	/* The gutter has to match too, or two grids of identical track width still land their columns in
	   different places: with auto-fill, the gap is part of what decides how many tracks fit. */
	--card-gap: 1rem;

	flex: 1;
	min-height: 0;
	overflow-y: auto;
	/* Tighter gutters than a reading layout would take. This is a workbench rather than a document:
	   the useful thing on screen is the grid, and every rem of gutter is a card we are not showing.
	   No max-width either -- a library benefits from a wide window, and anyone who wants a narrower
	   measure has one available by making the window narrower. */
	/* Named, because a `position: sticky; bottom: 0` inside this region sticks to the edge of the
	   scrollport LESS this padding -- every browser deflates the sticky rectangle by the scroll
	   container's padding -- and the one control that wants the true bottom (.membership-save)
	   reaches past it by exactly this amount. One number, two readers. */
	--main-padding-bottom: 3rem;
	/* The top clears the toolbar lying over it (.app-content). Sticky `top: 0` children land under
	   the toolbar rather than behind it for the same deflation, and scroll-padding does it for a
	   jump to an anchor. */
	padding: calc( var( --toolbar-overlap ) + 1rem ) 1rem var( --main-padding-bottom );
	scroll-padding-top: var( --toolbar-overlap );
	scroll-behavior: smooth;
}

/* Stale content while the next view loads: dimmed, not blanked, and not interactive. Blanking on
   navigation is what makes a single-page app feel worse than the page loads it replaced. */
.app-main[data-stale="true"]
{
	opacity: .55;
	pointer-events: none;
}

.view-heading
{
	display: flex;
	align-items: baseline;
	gap: .6rem;
	flex-wrap: wrap;
	margin: 0 0 .3rem;
}

.view-heading h1
{
	margin: 0;
	font-size: var( --text-xl );
	letter-spacing: -.015em;
}

/* MINE · EVERYONE -- the uploads view's audience switch (D386), and the queue page's status colours.
   Two links drawn as one segmented control; `aria-current` is what draws the pressed one, so the
   look and the announcement are one attribute. The queue page borrows the tray's glyph colours by
   status, on the row's `data-job-status`, so a failure is found by its colour in the order it ran. */
.view-switch
{
	display: inline-flex;
	gap: .2rem;
	margin: .1rem 0 1rem;
	padding: .2rem;
	border: 1px solid var( --rule );
	border-radius: 999px;
	background: var( --card-background );
}

.view-switch-option
{
	padding: .3rem .85rem;
	border-radius: 999px;
	font-size: var( --text-base );
	font-weight: 600;
	color: var( --ink-quiet );
	text-decoration: none;
}

.view-switch-option:hover { color: var( --ink ); }
.view-switch-option[aria-current="page"] { background: var( --accent-quiet ); color: var( --ink ); }
.view-switch-option:focus-visible { outline: 2px solid var( --accent ); outline-offset: 1px; }

.queue-page tr[data-job-status="running"] .queue-status { color: var( --accent ); font-weight: 600; }
.queue-page tr[data-job-status="failed"] .queue-status,
.queue-page tr[data-job-status="dead"] .queue-status { color: var( --warning ); font-weight: 600; }
.queue-page tr[data-job-status="queued"] .queue-status { color: var( --ink-quiet ); }

/* A stopped row nobody needs to answer -- acknowledged, or superseded by a later run (D393) -- is
   drawn quiet, so the warning colour is reserved for what the badge is counting. */
.queue-page tr[data-job-settled="true"] .queue-status { color: var( --ink-quiet ); font-weight: 400; }
.queue-page tr[data-job-settled="true"] td { color: var( --ink-quiet ); }
.queue-ok-form { display: inline; margin: 0 0 0 .35rem; }
.queue-ok { font-size: var( --text-xs ); padding: .2rem .5rem; }
.queue-acknowledged { font-size: var( --text-sm ); color: var( --ink-quiet ); margin-left: .35rem; }

.view-subtitle
{
	margin: 0 0 1.2rem;
	color: var( --ink-quiet );
	font-size: var( --text-base );
}

/* The line under "All team items" and "All project items" (D492): where these cards live, said
   once, quietly, between the heading and the grid. */
.section-byline
{
	margin: -.35rem 0 .9rem;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
}

/* The container description (schema v22, D320): prose under the metadata line, height-limited so a
   long one scrolls inside its own box rather than pushing the action bar and the grid below the
   fold -- the description is front matter, and front matter does not get to be the page. The
   rendered subset is lib/markdown.php's; the element selectors below are the whole of that subset,
   sized one step under the page's own headings. */
.scope-description
{
	max-width: 46rem;
	max-height: 11rem;
	overflow-y: auto;
	margin: -.4rem 0 1.2rem;
	padding: .6rem .85rem;
	border: 1px solid var( --rule );
	border-radius: .5rem;
	background: color-mix( in srgb, var( --ink-quiet ) 4%, transparent );
	line-height: 1.5;
	font-size: var( --text-md );
}

.scope-description > :first-child { margin-top: 0; }
.scope-description > :last-child { margin-bottom: 0; }
.scope-description p, .scope-description ul, .scope-description ol, .scope-description blockquote { margin: 0 0 .6rem; }
.scope-description h2, .scope-description h3, .scope-description h4 { margin: .4rem 0 .3rem; font-size: 1em; }
.scope-description h2 { font-size: 1.08em; }
.scope-description ul, .scope-description ol { padding-left: 1.4rem; }
.scope-description blockquote { padding-left: .8rem; border-left: 3px solid var( --rule ); color: var( --ink-quiet ); }
.scope-description code { font-size: .92em; }

/* THE USER GUIDE (D322): the same Markdown subset as the description above, drawn as a page rather
   than as front matter -- no box, no height limit, a reading measure, and the headings step down in
   size the way a document's do, because here they ARE the document's structure and not a decoration
   inside a paragraph or two. A typed `#` lands as h2 under the view's own h1 (lib/markdown.php).
   The fenced code block is the one element the description box never draws; it exists for the
   guide, to show an address or a shortcut exactly as typed. */
.guide-prose
{
	max-width: 44rem;
	line-height: 1.55;
	font-size: var( --text-md );
}

/* FOLDER ACTIONS (D342): the placeholder page reads like the guide -- a measure, a line height and
   headings that step down -- because it IS a page of prose about what is coming, and a page dressed
   as a form with nothing in it would promise controls it does not have. Its own class rather than
   `.guide-prose`, so that the guide's typography can change without restyling a page that only
   happens to look like it today. */
.actions-prose
{
	max-width: 44rem;
	line-height: 1.55;
	font-size: var( --text-md );
}

.actions-prose p, .actions-prose ul { margin: 0 0 .9rem; }
.actions-prose h2 { margin: 1.6rem 0 .5rem; font-size: var( --text-lg ); letter-spacing: -.01em; }
.actions-prose ul { padding-left: 1.5rem; }
.actions-prose li { margin: 0 0 .35rem; }

.guide-prose > :first-child { margin-top: 0; }
.guide-prose p, .guide-prose ul, .guide-prose ol, .guide-prose blockquote, .guide-prose pre { margin: 0 0 .9rem; }
.guide-prose h2 { margin: 2rem 0 .6rem; font-size: var( --text-lg ); letter-spacing: -.01em; }
.guide-prose h3 { margin: 1.4rem 0 .5rem; font-size: var( --text-md ); }
.guide-prose h4 { margin: 1.1rem 0 .4rem; font-size: var( --text-md ); }
.guide-prose ul, .guide-prose ol { padding-left: 1.5rem; }
.guide-prose li { margin: 0 0 .3rem; }
.guide-prose blockquote { padding-left: .9rem; border-left: 3px solid var( --rule ); color: var( --ink-quiet ); }
.guide-prose code { font-size: .9em; }
.guide-prose pre
{
	padding: .7rem .9rem;
	border: 1px solid var( --rule );
	border-radius: .5rem;
	background: color-mix( in srgb, var( --ink-quiet ) 6%, transparent );
	overflow-x: auto;
	line-height: 1.45;
}
.guide-prose pre code { font-size: .88em; }
.guide-missing { color: var( --ink-quiet ); }

/* THE GUIDE'S PAGE AND ITS CONTENTS (2026-10-01). The prose keeps its reading measure and the page is
   centred in the main region; beside it, on the right, a short list of the guide's sections that
   stays in view while the prose scrolls (guideview.php's renderUserGuideContents()). The centring
   used to be experiments.css's and moved here with the aside, because the two are one layout.

   The breakpoint is a CONTAINER query on .guide-frame, not a media query: what decides whether an
   aside fits is the main region's width, and that changes with the sidebar as much as with the
   window. Narrower than the two columns need, the contents sit above the prose as an ordinary list,
   not sticky -- a pinned list over a phone's prose would cover the thing it indexes.

   Sticky works because .app-main is the shell's only scroll container; `top: 0` lands at its padding
   edge. The list scrolls on its own if it is ever taller than the window. */
.guide-frame { container-type: inline-size; }

.guide-layout
{
	display: grid;
	grid-template-columns: minmax( 12rem, 44rem ) 13rem;
	grid-template-areas: "title ." "prose contents";
	justify-content: center;
	align-items: start;
	column-gap: 3rem;
}

.guide-layout > .guide-heading { grid-area: title; }
.guide-layout > .guide-prose { grid-area: prose; min-width: 0; }

.guide-contents
{
	grid-area: contents;
	position: sticky;
	top: 0;
	max-height: calc( 100dvh - 9rem );
	overflow-y: auto;
	font-size: var( --text-sm );
	line-height: 1.4;
}

.guide-contents-title
{
	margin: 0 0 .5rem;
	color: var( --ink-quiet );
	font-weight: 600;
}

.guide-contents ol { margin: 0; padding: 0; list-style: none; }
.guide-contents ol ol { margin: .2rem 0 .3rem .8rem; }
.guide-contents li { margin: 0 0 .25rem; }
.guide-contents a { color: inherit; text-decoration: none; }
.guide-contents a:hover, .guide-contents a:focus-visible { text-decoration: underline; }

/* A picture or a video placed in the guide (guideview.php renderUserGuideMedia()): the column's width
   at most, its own shape kept, the caption quiet beneath it. */
.guide-media { margin: 1.2rem 0 1.4rem; }
.guide-media img, .guide-media video { display: block; max-width: 100%; height: auto; border-radius: .5rem; }
.guide-media figcaption { margin-top: .45rem; color: var( --ink-quiet ); font-size: var( --text-sm ); }
.guide-media-missing { color: var( --ink-quiet ); font-style: italic; }

/* A section reached through the contents lands with a little air above it, not flush to the edge. */
.guide-prose h2, .guide-prose h3 { scroll-margin-top: 1rem; }

@container ( max-width: 62rem )
{
	.guide-layout
	{
		grid-template-columns: minmax( 0, 44rem );
		grid-template-areas: "title" "contents" "prose";
	}

	.guide-contents
	{
		position: static;
		max-height: none;
		padding: 0 0 1rem;
		margin-bottom: 1rem;
		border-bottom: 1px solid var( --rule );
	}

	/* Above the prose, the list is something to scroll past; the sections are enough of it. */
	.guide-contents ol ol { display: none; }
}

.view-section
{
	margin: 0 0 2rem;
}

.view-section > h2
{
	margin: 0 0 .7rem;
	font-size: var( --text-md );
	letter-spacing: -.005em;
}

.view-section > h2 .count
{
	color: var( --ink-quiet );
	font-weight: 400;
}


/* THE ARCHIVED SECTION (D317): a <details> at the foot of a listing, its heading carrying the count
   of rows the page did not load. The <summary> wears the section's own <h2> so that "Archived 12"
   reads as one more heading in the column above it, with a chevron where the disclosure is -- the
   same glyph the sidebar's sections and rows turn, for the same meaning. Muted at rest, the way an
   archived row is everywhere else (.sidebar-row[data-archived] above): finished work is quieter
   than live work, and a heading you have not opened is the quietest state it has. */
.archived-section > summary
{
	display: flex;
	align-items: center;
	gap: .45rem;
	cursor: pointer;
	list-style: none;
	opacity: .7;
}

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

/* Two glyphs rather than one rotated: task9 ratchets the stylesheet to exactly two rotations, both
   in the sidebar and both ending at an icon (D176's trap), and a third would be a third thing to
   get wrong the same way. A triangle is stated, not pressable, and the whole summary is the target. */
.archived-section > summary::before
{
	content: '▸';
	flex: 0 0 auto;
	width: .9rem;
	color: var( --ink-quiet );
	font-size: var( --text-base );
	line-height: 1;
}

.archived-section[open] > summary::before { content: '▾'; }

.archived-section > summary:hover,
.archived-section[open] > summary
{
	opacity: 1;
}

.archived-section > summary > h2 { margin: 0; }

.archived-section-body { margin-top: .7rem; }


/* ---------------------------------------------------------------------------------------------
   Badges, chips and indicators.
   --------------------------------------------------------------------------------------------- */

/* THE VOCABULARY RULE, and it is one line long:

       round is pressable, square is stated.

   Before this, a badge and a control were both bordered pills, so "DRAFT" was indistinguishable from
   "1080p" and an audit of this very file turned up two genuine controls wearing .badge -- the upload
   picker and the "People and grants" link (decisions D129). An outline is what reads as a hit
   target, so a badge has none and takes a square corner; anything pressable keeps the pill, the
   border and a hover.

   Defined HERE and nowhere else. item.css used to carry a second .badge block, which is the
   two-sources-for-one-fact arrangement this project keeps finding the wrong end of. */

.badge
{
	display: inline-flex;
	align-items: center;
	gap: .25rem;
	padding: .14rem .42rem;
	border: 0;
	border-radius: 3px;
	/* Quiet on purpose. A badge is metadata: it should be legible at a glance and never compete
	   with a control on the same row, which is the inversion D135 was about. */
	background: color-mix( in srgb, var( --ink-quiet ) 8%, transparent );
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	font-weight: 600;
	letter-spacing: .045em;
	white-space: nowrap;
}

/* The pressable counterpart: round, bordered, hoverable. For a control that is not a <button> --
   a <label> wrapping a hidden file input, a navigation link styled as a chip -- so the shape says
   what the element does rather than what tag it happens to be. */

.control-pill
{
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	padding: .2rem .6rem;
	border: 1px solid var( --rule );
	border-radius: 999px;
	background: var( --card-background );
	color: var( --ink );

	/* `font: inherit` before the size, and it is not tidiness: a <button> does not inherit its font
	   family, so every button wearing this class was drawn in the browser's default UI face while
	   every <a> and <label> wearing it was drawn in ours. One class, two typefaces, and the
	   difference only shows where the two sit side by side -- which is where it was found. */
	font: inherit;
	font-size: var( --text-sm );
	text-decoration: none;
	white-space: nowrap;
	cursor: pointer;

	/* The system height (see --control-height), which the fields below it share. `justify-content:
	   center` because a pill that is taller than its text has to say where the text sits, and
	   centred is the only answer that survives the label changing length. */
	min-height: var( --control-height );
	justify-content: center;
}

.control-pill:hover { border-color: var( --ink-quiet ); }
.control-pill:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }


/* ---------------------------------------------------------------------------------------------
   THE FOUR TONES A CONTROL CAN WEAR (Jon, 2026-09-04).

   Until now there were two -- accent and warning -- and they were carrying more meanings than two
   colours can hold. "New folder" and "Save" were the same green; "Erase permanently" and the
   "Suspended" badge were the same red-brown; and everything else was the same uncoloured pill
   whether it was a real secondary choice or just a control nobody had got round to toning. So the
   vocabulary is now four, and each one answers a different question about what pressing it does:

     (no tone)              NEUTRAL. A secondary choice, or an ordinary navigation. Colourless on
                            purpose -- it is the ground the other three are read against, and if
                            everything is toned then nothing is. This is also where a control that
                            reads as bare text belongs: it becomes a pill in the neutral tone
                            rather than staying a link that happens to submit.
     data-tone="create"     GREEN. It brings something into existence -- a folder, a project, an
                            account, a passkey, a link. Takes a `+` where the shape allows one.
     data-tone="accent"     ACCENT. It acknowledges, saves, or moves to the next step. This is the
                            page's own colour, and a surface should offer at most one of these.
     data-tone="danger"     RED. It takes something away. See --danger for why this is not
                            --warning: a tone that means "be careful" and a tone that means "this
                            deletes" are two vocabularies, and one token cannot hold both.

   One shape for all four, and only the colour changes. That is the point -- a person should be able
   to tell what a control DOES from a glance at its hue without having to also decode a second
   difference in its size or its fill.

   `data-tone="warning"` is kept below as an alias of `danger` and is deliberately not deleted: a
   toned pill rendered by a caller this pass missed would otherwise silently lose its tone, which is
   exactly the failure mode D129 found the last time this vocabulary moved.
   --------------------------------------------------------------------------------------------- */

/* A HOVER TINT IS DERIVED FROM ITS TONE, NEVER READ FROM A SECOND TOKEN, and this is a defect found
   the same day it was introduced. The three hovers first read `--create-quiet`, `--accent-quiet` and
   `--danger-quiet` -- which is fine while one hand maintains both halves, and wrong the moment
   anything overrides only one of them. `experiments.css` does exactly that: it sets `--accent` to a
   cyan and leaves `--accent-quiet` commented out, so an accent pill was drawn cyan and hovered to
   the GREEN tint app.css ships. `color-mix` from the tone itself cannot come apart, because there is
   only one value to override. The `-quiet` tokens keep their own jobs (badges, notices, banners),
   where the pairing is stated deliberately rather than assumed. */
.control-pill[data-tone="create"]
{
	border-color: var( --create );
	color: var( --create );
	background: color-mix( in srgb, var( --create ) 8%, var( --card-background ) );
	font-weight: 600;
}

.control-pill[data-tone="create"]:hover { border-color: var( --create ); background: color-mix( in srgb, var( --create ) 16%, var( --card-background ) ); }

.control-pill[data-tone="accent"]
{
	border-color: var( --accent );
	color: var( --accent );
	background: color-mix( in srgb, var( --accent ) 8%, var( --card-background ) );
	font-weight: 600;
}

.control-pill[data-tone="accent"]:hover { border-color: var( --accent ); background: color-mix( in srgb, var( --accent ) 16%, var( --card-background ) ); }

/* A STATE, NOT A VERB, and the one tone outside the four above: a control whose thing is in use --
   Share folder on a folder that has a live share (Jon, 2026-10-08). It reads --in-use, a palette
   token every theme declares, blue because --accent is green in the base theme and the state was
   asked for in blue. The shades are measured at 4.5:1 on this rule's own 8% tint, not on the bare
   card, which is where the first value (#2a6fdb, the legend's blue) fell short. */
.control-pill[data-tone="in-use"]
{
	border-color: var( --in-use );
	color: var( --in-use );
	background: color-mix( in srgb, var( --in-use ) 8%, var( --card-background ) );
	font-weight: 600;
}

.control-pill[data-tone="in-use"]:hover { border-color: var( --in-use ); background: color-mix( in srgb, var( --in-use ) 16%, var( --card-background ) ); }

/* The destructive tone was being SET and never drawn: the one control wearing it was "Remove
   <name>", the button at the bottom of a removal confirmation, and it rendered identically to "Save
   the name". A destructive submit that looks like every other submit is the confirmation panel
   doing half its job -- the panel spends a paragraph saying what will be lost and then hands over a
   neutral button. Same shape as the other tones, in the danger colour, so the four are visibly one
   vocabulary rather than four ideas about what a toned pill is. */
.control-pill[data-tone="danger"],
.control-pill[data-tone="warning"]
{
	border-color: var( --danger );
	color: var( --danger );
	background: color-mix( in srgb, var( --danger ) 8%, var( --card-background ) );
	font-weight: 600;
}

.control-pill[data-tone="danger"]:hover,
.control-pill[data-tone="warning"]:hover { border-color: var( --danger ); background: color-mix( in srgb, var( --danger ) 16%, var( --card-background ) ); }

/* THE DEFAULT KEY, and it is a fill rather than a fifth colour.

   ui-design.md §4.3 already says where a default lives -- rightmost in the dock -- and that the
   accent is a promise about what Enter does. What it did not say is how a default is drawn when the
   surface it sits on already has accent-toned controls on it, and the answer cannot be another hue:
   a filled accent is the same PROMISE stated louder, where a fifth colour would be a fifth meaning.

   So `data-default="true"` is a modifier on a tone rather than a tone of its own, it composes with
   whichever tone the control already wears, and the tone still says what the button does -- "Move
   it anyway" is filled AND danger, because it is both the default and destructive, and those are
   two facts a person needs at once.

   The border stays 1px and the padding does not change, so promoting a button (which signin.js does
   at runtime, D258) cannot move anything -- except that a default also takes the confirm floor
   (`min-width`, beside .button-dock), so promoting a button whose word is shorter than the floor
   widens it to the floor. The sign-in verbs are all longer than it, so the D258 case still holds. */
.control-pill[data-default="true"]
{
	background: var( --accent );
	border-color: var( --accent );
	color: var( --card-background );
	font-weight: 600;
}

.control-pill[data-default="true"]:hover
{
	background: color-mix( in srgb, var( --accent ) 85%, var( --ink ) );
	border-color: color-mix( in srgb, var( --accent ) 85%, var( --ink ) );
}

.control-pill[data-default="true"][data-tone="create"]
{
	background: var( --create );
	border-color: var( --create );
	color: var( --card-background );
}

.control-pill[data-default="true"][data-tone="danger"],
.control-pill[data-default="true"][data-tone="warning"]
{
	background: var( --danger );
	border-color: var( --danger );
	color: var( --card-background );
}

/* The `+` that rides in a create pill, and it is a real glyph rather than the `plus` icon for one
   reason: the icon sprite is drawn at --icon-size and aligns to the middle of the pill, which is
   right for a folder or a person -- a NOUN standing beside a label -- and slightly wrong for an
   operator, which wants to sit on the text's own optical centre. A control that has no natural noun
   takes this instead of an icon; one that has a noun (New folder takes a folder) keeps the icon and
   omits this, because a pill carrying two marks is a pill with a decoration on it. */
.control-pill .pill-plus
{
	font-size: 1.05em;
	line-height: 1;
	/* The glyph's own bearings sit it high in the line box; this is the optical correction, and
	   it is small enough that it changes nothing about the pill's height. */
	margin-top: -0.06em;
}


/* EVERY OTHER BUTTON ON THE PAGE, and this rule is a safety net rather than the design.

   The design is that a pressable thing wears a class that says what it is -- .control-pill,
   .sidebar-row, .rendition, .layout-choice. But an audit on 2026-09-04 found eight <button>s
   carrying no class at all: the share gate's submit, "Copy" beside a share link, "Revoke", "Withdraw",
   the members panel's "Remove" (which carried data-tone="warning" and no class, so the tone drew
   NOTHING), and the diagnostics panel's pair. Each of those rendered in whatever the browser draws
   -- platform blue on a Mac, grey-on-grey on Linux -- which is the one appearance on this page
   nobody here chose, and they are all now given their class at the call site.

   This stays anyway, and `:not( [class] )` is what makes it safe: it can only ever match a button
   that has been given no class, so it can never fight a rule that has. A classless button appearing
   in future is a small omission that draws correctly rather than a UA control in the middle of the
   interface. `all: unset` is deliberately avoided -- it would take the button's keyboard behaviour
   with it -- so this restates the pill's own declarations instead. */
button:not( [class] ),
input[ type="submit" ]:not( [class] )
{
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	padding: .2rem .6rem;
	border: 1px solid var( --rule );
	border-radius: 999px;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	white-space: nowrap;
	cursor: pointer;
}

button:not( [class] ):hover { border-color: var( --ink-quiet ); }
button:not( [class] ):focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }

/* ---------------------------------------------------------------------------------------------
   THE ACTION ROW -- ui-design.md §4.2, from Jon's mockup (D240, first built at D278).

   A button says the verb; something visible beside it says the rest. The shape it replaces was
   button-carries-the-whole-sentence, and two things were wrong with it: a hit target whose width is
   set by a sentence can never line up with another one, so no column of them is possible at any
   width, and the explanation trailing beside it was doing the work of a label while looking like a
   footnote.

   NO BREAKPOINT, deliberately. These sit inside tool areas, which are themselves in a
   `repeat( auto-fit, ... )` grid, so a viewport media query can only GUESS at the width available
   and a container query would be machinery answering a question nobody is asking. Wrapping answers
   it intrinsically: when the text cannot hold its basis the button wraps onto a flex line of its own.

   THE BUTTON STAYS ON THE RIGHT (D636, Jon: "even in a quite narrow column, we should try to match
   the layout of tool offers with buttons right aligned for visual clarity"). The text's basis is 9rem
   (it was 18rem), so a row keeps text-left, button-right down to a narrow column; and when it must
   wrap, the controls take `margin-left: auto` and land at the right edge rather than under the
   title, which is what the 18rem basis used to put there.

   BASELINE, not centre. With one row and a two-line detail the two are indistinguishable; the
   deciding case is a stack, where under `center` a button's vertical position depends on how long
   its own detail is -- so a column of them is ragged and adding a sentence silently moves a hit
   target. Under `baseline` every verb sits beside the title it completes.

   The title is NOT pressable. Round is pressable, square is stated: the hit target is the pill, and
   only the pill.
   --------------------------------------------------------------------------------------------- */
.action-row
{
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: baseline;
	gap: .6rem 1.2rem;
	margin: 0;
	padding: .9rem 0;

	/* A hairline between rows and none after the last, which is the separator the lifecycle forms
	   already used. Reusing it rather than inventing one keeps the two areas one vocabulary. */
	border-bottom: 1px solid var( --rule );
}

.action-row:last-child { padding-bottom: .15rem; border-bottom: 0; }

/* 46rem is the page's existing prose measure, and the 18rem basis is what decides when the button
   wraps. */
.action-row-text { flex: 1 1 9rem; max-width: 46rem; }

/* Not a heading element and not styled as one: it names an action rather than opening a section,
   and a heading here would put three fake subsections inside one tool area's outline. */
.action-row-title
{
	margin: 0;
	font-size: var( --text-base );
	font-weight: 600;
	color: var( --ink );
}

.action-row-detail
{
	margin: .25rem 0 0;
	font-size: var( --text-base );
	line-height: 1.5;
	color: var( --ink-quiet );
}

/* The button, or -- where the action is refused -- the badge that takes its place in the same
   column at the same width. `affordances.php` has drawn `blocked` as a statement rather than a
   greyed-out control since D133; this is the first shape that lets the statement carry its reason. */
/* A COLUMN OF BUTTONS IS A COLUMN, and every button in it is the same width (Jon, 2026-09-04).

   The deciding case is the Sunsetting area: Archive, Remove and Erase permanently, three rows
   stacked one above the other, each with its verb in the right-hand column and each verb a
   different length -- so the three hit targets formed a ragged edge down the page and the eye had
   to find each one separately. The comment above already chose `baseline` over `center` for exactly
   this reason (a column of controls must not move as its sentences change length); this is the
   horizontal half of the same argument, and it was missing.

   `min-width` rather than `width`, so a verb longer than the basis still fits rather than being
   clipped. `center`, because a left-aligned label inside a padded pill of fixed width reads as a
   text field.

   It applies to the BADGE as well as to the pill: the refused state puts a badge in the column the
   button would have occupied, and a statement narrower than the controls above it re-ragged the
   very edge this rule exists to straighten.

   9.5rem is the longest verb this interface currently puts in such a column ("Erase permanently" at
   .78rem) plus its padding, rounded up. It is a floor and not a measurement: a longer verb widens
   the column and every member of it widens together, which is the whole behaviour being bought. */
.action-row > .control-pill,
.action-row > .badge
{
	flex: 0 0 auto;
	margin-left: auto;
	min-width: 9.5rem;
	justify-content: center;
	text-align: center;
}

/* TWO BUTTONS IN THE ONE COLUMN, stacked. The cropper row is the only one that needs it: while its
   stage is open the verb is "Done" and "Cancel" belongs directly beneath it, which is the ordinary
   shape of a disclosure that has to be confirmed or abandoned.

   It repeats the width rule above rather than widening that selector to `.action-row .control-pill`,
   because a descendant selector there would also catch any pill a row's DETAIL column ever contains
   and force it to 9.5rem -- the rule is about the right-hand column, not about pills in general.

   `baseline` on the row makes the first button's text line up with the title exactly as a lone pill
   does, so an open cropper does not shift the row's own alignment. */
.action-row > .action-row-controls
{
	flex: 0 0 auto;
	margin-left: auto;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: .4rem;
}

.action-row-controls > .control-pill
{
	min-width: 9.5rem;
	justify-content: center;
	text-align: center;
}

/* AN OFFER'S OWN FIELD (D390): the transcript's language selector, standing in the stack under its
   button with no label of its own (Jon, 2026-09-14 -- its default option is the caption). It keeps the
   button's width, because the two are one column and a narrower select would read as belonging to
   nothing. */
.action-row-controls > .action-row-select
{
	min-width: 9.5rem;
	max-width: 14rem;
}

/* The trap this file names in four other places: `.control-pill` sets `display: inline-flex`, and an
   author rule beats the user agent's `[hidden] { display: none }` whatever the specificity -- so
   Cancel would sit there, visible, with a hidden attribute on it. */
.action-row-controls > .control-pill[hidden] { display: none; }

/* WHY NOT, as a second detail line. A refused action keeps its title and its detail exactly where
   they were -- what it would do is still worth knowing -- and this is the line that says why it
   cannot. Quieter than the detail above it and marked with a rule, so the eye can tell the two
   apart without reading either: one is about the action, the other is about the world. */
.action-row-refusal
{
	margin-top: .35rem;
	padding-left: .6rem;
	border-left: 2px solid color-mix( in srgb, var( --ink-quiet ) 35%, transparent );
}

/* ---------------------------------------------------------------------------------------------
   THE BADGE IN THE BUTTON'S COLUMN: `working`, `ready`, `blocked`.

   These three moved here from `.affordance[data-state=...]` in item.css when every offer became an
   action row. The weights they carried are the point and they survive the move intact (D135's
   ladder): a statement must never out-weigh a control on the same row, and a badge is already the
   quietest thing this vocabulary has. What changes is only that the statement now sits in the
   column the button would have occupied, at the same width, beside a title and a detail that no
   longer vanish when there is nothing to press.
   --------------------------------------------------------------------------------------------- */

/* `working`: a slow light travelling left to right, rather than an opacity pulse. The difference is
   not decorative -- a pulse has no direction, and a blinking control reads as an alarm as readily as
   as progress. A sweep advances, which is the thing being claimed, and it is the honest amount of
   information to claim: the queue does not report a percentage. Background only, so nothing moves in
   the layout and the word stays readable throughout. */
.badge[data-state="working"]
{
	background-color: color-mix( in srgb, var( --ink-quiet ) 10%, transparent );
	background-image: linear-gradient
	(
		100deg,
		transparent 25%,
		color-mix( in srgb, var( --accent ) 26%, transparent ) 48%,
		transparent 71%
	);
	background-size: 220% 100%;
	background-repeat: no-repeat;
	animation: affordance-sweep 2.1s linear infinite;
}

@keyframes affordance-sweep
{
	from { background-position: -70% 0; }
	to   { background-position: 190% 0; }
}

/* Respect the reader. A sweeping badge is decoration, and vestibular disorders are not rare.
   Reduced motion keeps the tint -- so the state is still legible -- and stops the movement. */
@media ( prefers-reduced-motion: reduce )
{
	.badge[data-state="working"]
	{
		animation: none;
		background-image: none;
	}
}

.badge[data-tone="accent"]
{
	background: var( --accent-quiet );
	color: var( --accent );
}

.badge[data-tone="warning"]
{
	background: var( --warning-quiet );
	color: var( --warning );
}

.badge[data-tone="quiet"]
{
	background: color-mix( in srgb, var( --ink-quiet ) 7%, transparent );
}

/* The indicator strip on a card. Glyph plus label, and the label is never hidden behind a hover:
   an icon whose meaning is only in a tooltip is an icon most people will misread. The title
   attribute carries the *reason*, which is the part that does not fit. */
.indicators
{
	display: flex;
	flex-wrap: wrap;
	gap: .25rem;
	margin: 0;
}

.indicator
{
	display: inline-flex;
	align-items: center;
	gap: .2rem;
	font-size: var( --text-xs );
	color: var( --ink-quiet );
	cursor: help;
}

.indicator[data-tone="accent"]
{
	color: var( --accent );
}

.indicator[data-tone="warning"]
{
	color: var( --warning );
}

/* An archived container's tile. Muted like its sidebar row, and by the same attribute, so the two
   representations of one folder cannot drift apart visually. */
.scope-tile[data-archived="true"]
{
	opacity: .62;
}

.scope-tile[data-archived="true"]:hover
{
	opacity: 1;
}

/* THE FOOTER UNDER A LISTING (D553): "60 of 423 · Show more", "All 423", or the sentence that asks
   the reader to narrow the list at the ceiling. The count is quiet, the way out is a pill, and the
   whole line is a status region so the count's change is what a screen reader hears. The same
   footer closes a search's bounded group with "See all 34 folders". */
.listing-footer
{
	display: flex;
	gap: .8rem;
	align-items: center;
	flex-wrap: wrap;
	margin: 1rem 0 0;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

.listing-footer.is-loading .listing-more
{
	opacity: .55;
	pointer-events: none;
}

/* "All uploads" beside a preview's heading: the way from a bounded rail to the listing it previews. */
.section-see-all
{
	margin-left: .6rem;
	font-size: var( --text-sm );
	font-weight: normal;
	color: var( --ink-quiet );
	text-decoration: underline;
	text-underline-offset: .15em;
}

.section-see-all:hover { color: var( --ink ); }

/* The Archive view's footnote about what it does NOT list. A footnote, so it reads as one. */
.view-note
{
	margin: 1.2rem 0 0;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

/* The archived badge (D271). Quiet on purpose and by name: nothing is wrong, nothing is unavailable,
   and a colour that suggested either would contradict the sentence in its own tooltip. */
.indicator[data-tone="quiet"]
{
	color: var( --ink-quiet );
}

/* WHAT THIS ITEM HAS, as opposed to what is true of it.

   The result badges name the tool areas an item page would draw for this item -- Key image, Web
   video, Rendered audio -- and they are the one group in the strip that describes an artefact
   rather than a property. So they are held slightly apart: a chip with a ground under it, where
   everything else in the row is bare text. Quiet enough to stay behind the two live states, which
   keep the accent colour and are the only things here a person is waiting on.

   The dot is deliberately not a tick: a tick reads as approved, and nothing has approved anything.
   It says only that the thing exists.

   **No chip any more (D415).** The ground under it read as a block of colour in a row of quiet text
   and Jon called the strip untidy; since D414 these badges are only the extras somebody asked for,
   and quiet text with the dot is enough to hold them apart.

   **And a pill again, in the card footer only (2026-09-22)** -- with every other indicator beside
   it, which is the difference: one chip in a row of bare text was a block of colour, and a row
   where everything is a pill is a row. The list's Status column keeps the bare text; see
   `.card-footer`. */
.indicator[data-tone="result"]
{
	gap: .25rem;
	color: var( --ink );
}

.indicator[data-tone="result"] .glyph
{
	font-size: .5rem;
	color: var( --ink-quiet );
}

.indicator .glyph
{
	font-size: var( --text-sm );
	line-height: 1;
}

/* The uploader chip: initials, because a name on every card in a grid of sixty is noise and an
   avatar we do not have is a grey circle. Initials plus a title attribute is honest and legible. */
.person-chip
{
	display: inline-grid;
	place-items: center;
	width: 1.35rem;
	height: 1.35rem;
	border-radius: 50%;
	background: var( --selection-background );
	color: var( --ink );
	font-size: var( --text-2xs );
	font-weight: 700;
	letter-spacing: .02em;
	cursor: help;
}


/* ---------------------------------------------------------------------------------------------
   The item grid.
   --------------------------------------------------------------------------------------------- */

.item-grid
{
	/* The containing block for the reorder slot mover.js draws between two cards (`.mover-slot`
	   below, in the MOVING THINGS block). Nothing else in the grid is positioned against it. */
	position: relative;
	display: grid;
	/* Wider than the first pass. A 12.5rem card showed a 16:9 preview about 200 px across, which is
	   too small to tell two takes apart -- and telling two takes apart is the entire job of a
	   contact sheet. auto-fill still packs as many as the window allows, so a wide screen loses
	   nothing.

	   The measurement now lives on .app-main as --card-track, because the tile grids share it; the
	   fallback here is the same number, for a grid rendered outside the shell. */
	grid-template-columns: repeat( auto-fill, minmax( var( --card-track, 15.5rem ), 1fr ) );
	gap: var( --card-gap, 1rem );
	margin: 0;
	padding: 0;
	list-style: none;
}

.item-card
{
	position: relative;
	/* Two rows, the picture and the words, as a GRID rather than a flex column (Jon, 2026-09-18).
	   Nothing about the flow changes -- the body still takes what height is left. What a grid adds
	   is that the star and the kebab, which are absolutely positioned, can name the WORDS' row as
	   their area (`grid-row: 2`) and sit in the body's top right corner whatever height the
	   picture resolved to. `minmax( 0, 1fr )` so a long unbroken title cannot widen the card.

	   THREE ROWS SINCE 2026-09-22: the picture, the words, and the footer -- state and indicators as
	   a row of pills (Jon, from a Claude Design rendition). `auto 1fr auto` is what pins the footer
	   to the bottom edge: the words row takes the slack, so on a grid where every card in a row is
	   stretched to the tallest one, the pills sit at one height across the row whatever the titles
	   above them did. The star and the kebab still name row 2. */
	display: grid;
	grid-template-columns: minmax( 0, 1fr );
	grid-template-rows: auto 1fr auto;
	background: var( --card-background );
	border: 1px solid var( --rule );
	border-radius: .6rem;
	box-shadow: var( --shadow-card );
	overflow: hidden;
}

/* THE WHOLE CARD OPENS THE ITEM (D441), so the whole card has to say so. A surface that acts on a
   click while showing a text cursor is a surface nobody knows they can press -- the affordance
   follows the behaviour, and it is the same signal the preview and the title already gave as
   anchors. Every control inside the card sets its own cursor, so none of them inherits this. */
.item-card
{
	cursor: pointer;
}

.item-card[data-state="removed"]
{
	opacity: .6;
	border-style: dashed;
}

/* What moved while nobody was looking (D207).

   The shell refreshes itself once a person has been still for six seconds, and ui-design.md §1.2
   makes one condition on that: the highlight has to survive their return, or they arrive at a
   silently different grid -- which is the failure D96's pill was written to prevent, arriving
   quietly instead of under a cursor. So this is deliberately a persistent state and not an
   animation: it stays until the person clicks or types, which status.js treats as the
   acknowledgement.

   A ring rather than a fill, because the card's own state colours (removed, processing) already own
   its surface and a second background would fight them. `outline` rather than `border` for the same
   reason -- the border is the card's, it is 1px, and swapping its colour would move nothing but
   would make two rules disagree about who owns it. Outline is drawn outside the box, so nothing
   reflows when a mark appears or goes. */
.item-card[data-changed]
{
	outline: 2px solid var( --accent );
	outline-offset: 1px;
}

/* New and updated are told apart quietly rather than loudly. Something that ARRIVED is worth a
   stronger mark than something that merely changed state -- a card appearing is the only one of the
   two that moved everything after it. */
.item-card[data-changed="new"]
{
	outline-width: 3px;
}

@media ( prefers-reduced-motion: no-preference )
{
	/* One pass, then it stops. The mark itself persists; only the way it announces itself fades,
	   which is the difference between "look here" and a page that will not stop blinking. */
	.item-card[data-changed]
	{
		animation: card-changed-arrival .6s ease-out 1;
	}
}

@keyframes card-changed-arrival
{
	from { outline-color: transparent }
	to   { outline-color: var( --accent ) }
}

.item-card .card-preview
{
	position: relative;
	display: grid;
	place-items: center;
	/* 16:9, matching what almost everything here actually is. A 4:3 box around a 16:9 frame either
	   crops the sides off every video or pillarboxes it; matching the common case means the crop
	   falls on the unusual material instead of on all of it. Stills that are not 16:9 are still
	   covered and centred, which is the right compromise for a thumbnail whose job is recognition
	   rather than inspection. */
	aspect-ratio: 16 / 9;
	background: repeating-conic-gradient( var( --page-background ) 0% 25%, color-mix( in srgb, var( --page-background ) 60%, var( --card-background ) ) 0% 50% ) 50% / 1.25rem 1.25rem;
	overflow: hidden;
}

/*
 * [TRAP] Absolutely positioned, and that is the whole fix rather than a preference.
 *
 * The preview box is a grid with `aspect-ratio: 4 / 3`. An in-flow image with `height: 100%` needs a
 * DEFINITE parent height to resolve against, and a height derived from `aspect-ratio` is not
 * reliably that -- so the percentage falls back to the image's intrinsic height. Worse,
 * `aspect-ratio` sets a PREFERRED size, not a cap: an auto grid row grows to fit content that
 * insists, so the container itself stretched past its 4:3 box and the picture spilled over the card
 * body beneath it. Only visible on thumbnails that are not roughly 4:3 -- a portrait phone video is
 * 9:16, which is how this was found.
 *
 * Out of flow, an image cannot contribute to the row's height at all, so the box keeps its ratio and
 * `overflow: hidden` clips as intended. The parent already carries `position: relative`.
 */
.item-card .card-preview img
{
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/* THE THUMBNAIL'S ARRIVAL. The cards land in one paint; their pictures trickle in afterwards, and
   each one popping in at its own moment read as a grid that would not stop blinking. app.js marks
   every preview image still loading at render time `thumb-arriving` and lifts the mark when the
   bytes are in, so only the late arrivals fade and an image already in cache paints with its card,
   as crisply as before. The mark is applied BY SCRIPT ONLY -- with no JavaScript, nothing is ever
   held at opacity 0. `transition: none` on the marked state so the mark lands instantly however it
   is applied; only its REMOVAL animates, under the same motion gate as the card-arrival pass
   above. */
.item-card .card-preview img.thumb-arriving
{
	opacity: 0;
	transition: none;
}

@media ( prefers-reduced-motion: no-preference )
{
	.item-card .card-preview img
	{
		transition: opacity .3s ease-out;
	}
}

/* ---------------------------------------------------------------------------------------------
   THE CROP HINT (D365). Which part of the key image stands for the item, when somebody has said.

   No bytes are cut anywhere: the box already clips, so moving and scaling the picture inside it IS
   the crop, and the file underneath is the same file the item page shows whole. keyimage.php does
   the arithmetic -- four percentages, computed in PHP where the numbers already are -- and this rule
   only spends them.

   `inset: auto` first, because the base rule above pins all four sides and a `left` alone would be
   fighting a `right: 0`.

   `object-fit` is INHERITED FROM THE RULE ABOVE and stays `cover` on purpose. Once the img has been
   given a box of its own, `cover` and `fill` draw the identical picture whenever the rectangle's
   pixel aspect matches this box -- which the cropper enforces -- and they differ only when it is
   slightly off, where `cover` trims a hair and `fill` stretches by the same hair. Trimming is
   invisible; stretching a face is not.
   --------------------------------------------------------------------------------------------- */

.item-card .card-preview[data-key-crop] img
{
	inset: auto;
	left: var( --key-crop-left );
	top: var( --key-crop-top );
	width: var( --key-crop-width );
	height: var( --key-crop-height );
}

.item-card .card-preview .no-preview
{
	padding: .5rem;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	text-align: center;
}

/* ---------------------------------------------------------------------------------------------
   Where a preview would have been: the extension, big, and what kind of thing it is (D171).

   It replaced the words "No preview", which stated a fact about the interface rather than about the
   item -- and in a grid where nothing has a preview, every card looked exactly like every other. The
   extension is the one thing on the card that identifies the file, and it comes from the filename
   because the detected mimetype cannot: a .usdz is a zip.

   Sized in `em` off the card's own font so the same rule serves the grid and the item panel, which
   differ by an order of magnitude in the space they have.
   --------------------------------------------------------------------------------------------- */

.no-preview[data-kind="file"]
{
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: .25em;
	width: 100%;
	height: 100%;
}

.no-preview-extension
{
	font-size: 2.1em;
	font-weight: 650;
	letter-spacing: .04em;
	line-height: 1;
	color: var( --ink );

	/* Long extensions exist and must not blow the box out. */
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.no-preview-noun
{
	font-size: .95em;
	color: var( --ink-quiet );
}

.item-card .card-type
{
	--icon-size: .8rem;
	position: absolute;
	top: .4rem;
	/* The right corner rather than the left (Jon, 2026-09-22, folded in from experiments.css). The
	   person's own marks (D434) had that corner and move to the left, where the team's state stripe
	   is the only neighbour: a badge full of figures reads best against the edge a person reads
	   towards, and a plate of emoji reads fine anywhere. */
	right: .4rem;
	display: inline-flex;
	align-items: center;
	gap: .22rem;
	padding: .16rem .38rem;
	border-radius: .3rem;
	background: color-mix( in srgb, var( --card-background ) 82%, transparent );
	color: var( --ink-quiet );
	font-size: var( --text-2xs );
	letter-spacing: .04em;
	text-transform: uppercase;
	backdrop-filter: blur( 4px );
}

/* The file's own suffix, beside the family in the same badge (Jon, 2026-08-30).

   One badge and not two, and the divider between them is the point: `video` and `MP4` are two
   answers to one question -- what is this -- at two levels of precision, and a second floating pill
   in the corner would read as a second unrelated fact. The suffix is the stronger of the two,
   because the family is a bucket and the suffix is the file, so it takes the full ink colour while
   the family stays quiet. */
.item-card .card-type-suffix
{
	padding-left: .28rem;
	border-left: 1px solid var( --rule );
	color: var( --ink );
	font-weight: 650;
}

/* THE BADGE IS THE FILE'S OWN LINE NOW: `MP4 · 1080p · 4:07` (Jon, 2026-09-22; index.php on why the
   running time is there and the byte count is not). Three consequences for the rule above.

   The family WORD goes when there is a suffix to say it better -- the glyph beside it is the family
   already, and "VIDEO MP4 · 1080p · 4:07" is a badge that has stopped being a badge. It stays where
   there is no suffix (an upload still arriving), because then it is the only word there is.

   The divider goes with it: it divided the family from the suffix, and with the family hidden it
   would be a rule at the badge's left edge dividing nothing. The list layout's Kind column still
   draws the pair with the divider between them -- that cell is not inside `.card-type`.

   The facts after the suffix are quiet where the suffix is ink, for the badge's own reason: the
   suffix is what the file IS, and the rest is measurement. The " · " between them is drawn here so
   that a still with no running time is the same markup with a span fewer. */
.item-card .card-type:has( .card-type-suffix ) .card-type-family
{
	display: none;
}

.item-card .card-type:has( .card-type-suffix ) .card-type-suffix
{
	padding-left: 0;
	border-left: 0;
}

.item-card .card-type-fact
{
	color: var( --ink-quiet );
	font-weight: 500;
	letter-spacing: .02em;
	text-transform: none;
	/* Digits line up: `4:07` under `12:30` on the card beside it. */
	font-variant-numeric: tabular-nums;
}

.item-card .card-type-fact::before
{
	content: "·";
	margin: 0 .1rem 0 .04rem;
	color: var( --ink-quiet );
}

/* THE UPLOADER, IN THE BODY AND IN WORDS (Jon, 2026-09-18).

   It used to float in the bottom-left corner of the preview -- initials alone, with the name in a
   title attribute. index.php's own comment beside this row says why that is not enough on a card:
   two letters over a picture are a puzzle, and the answer was reachable only by a cursor that had
   already guessed there was one. So the chip keeps its shape and gains the name beside it, and the
   pair sits under the context trail rather than on the picture.

   Between the trail above and the meta line below in weight as well as in position: quieter than
   the title, and the name at the meta line's own size, because it is one more quiet fact about the
   item and not a heading. The chip is what the eye catches; the words are what it reads next.

   ITS OWN BLOCK, under a rule (Jon, 2026-09-22 -- the rule folded in from experiments.css, the
   rest from a Claude Design rendition). The title and the trail above the rule are what the item
   is and where it is; this row is who and when, "Jon Fabritius · 15 hours ago", one statement. The
   name is in ink now rather than quiet, because it is the answer to the question a shared library
   asks most and it was being whispered; the time stays quiet beside it. */
.item-card .card-person
{
	display: flex;
	align-items: center;
	gap: .4rem;
	min-width: 0;
	margin: .2rem 0 0;
	padding-top: .55rem;
	border-top: 1px solid var( --rule );
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	line-height: 1.25;
}

/* One line, ellipsised. A card is a fixed width and a display name is not: "Pia-Maria Rautavaara"
   wrapping to a second line would push the meta line down on one card in a row of five and make an
   even grid look broken. The time after it is what gets squeezed first, never wrapped. */
.item-card .card-person-name
{
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var( --ink );
	font-weight: 500;
}

.item-card .card-person-when
{
	flex: none;
	white-space: nowrap;
}

.item-card .card-person-when::before
{
	content: "· ";
}

.item-card .card-body
{
	display: flex;
	flex-direction: column;
	gap: .35rem;
	/* Roomier than it was (.55rem .6rem .6rem) since the body lost the strip to the footer and the
	   size to the badge: what is left is four lines, and they read better with air than packed. */
	padding: .75rem .8rem .7rem;
}

/* A META LINE WITH NOTHING ON IT TAKES NO ROOM. Since the size and the time became list-only cells
   (index.php, 2026-09-22) the line is empty on most cards -- it holds the view count, where there is
   one, and the folder where no trail says it -- and an empty <p> in a flex column still costs a gap.
   `:has()` on the cells the card layout draws rather than `:empty`, because the cells are there. */
body:not( [data-library-layout="list"] ) .card-meta:not( :has( .meta-cell:not( .meta-list-only ):not( :empty ) ) )
{
	display: none;
}

/* ---------------------------------------------------------------------------------------------
   THE FOOTER: state and indicators as pills, on the card's bottom edge (Jon, 2026-09-22).

   The legend line and the indicator strip were the body's last two children, so they sat wherever
   the title's wrapping left them. Now they are the card's third grid row (see .item-card), on a
   ground of their own, and everything on the row is a badge: the team's state, its keywords and
   the indicators all take the item page's .badge shape -- square-ish corners, one height -- because
   they are facts about the item and not buttons, and a rounded pill reads as something to press
   (Jon, 2026-09-23). The state leads and the item's own
   facts -- Private, Captions, what somebody made -- gather at the right, so a row of cards reads
   its states down the left edge and its facts down the right.

   ONLY IN THE CARD LAYOUT. The list dissolves this wrapper as it dissolves the body (§ below), so
   the strip stays the Status column and the chips stay the bare text a column wants; the grid and
   the icons hide it with the rest of the words. `body:not(...)` because the card layout is the
   default for markup that carries no attribute at all.
   --------------------------------------------------------------------------------------------- */
.item-card .card-footer
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .3rem .4rem;
	min-width: 0;
	padding: .5rem .8rem .55rem;
	border-top: 1px solid color-mix( in srgb, var( --rule ) 70%, transparent );
	background: color-mix( in srgb, var( --page-background ) 45%, var( --card-background ) );
}

/* No state, no words, no indicator: no row. index.php emits the wrapper with nothing between the
   tags for exactly this test. */
.item-card .card-footer:empty
{
	display: none;
}

/* ONE FLOWING LINE, not two groups. The first cut kept the legend line and the indicator strip as
   two flex items, state left and facts right as the rendition drew them -- and a card with a state
   and three indicators spent three lines on it, because the strip wrapped below the state as a
   block and then wrapped again inside itself (seen on a screen, 2026-09-22). With both <p>s
   dissolved, every pill is a flex item of the footer and they fill each line before starting the
   next: the state still leads, because it is first in the markup.

   Scoped away from the list, where `.indicators` is the Status column and must stay a box -- the
   unscoped selector would have outranked the list's own rule for it. */
body:not( [data-library-layout="list"] ) .card-footer .card-legend,
body:not( [data-library-layout="list"] ) .card-footer .indicators
{
	display: contents;
}

/* ONE HEIGHT FOR THE ROW: every badge is --text-xs on a 1.3 line with .18rem above and below, the
   state chip included (its padding in the legend block below matches), so a state beside an
   indicator does not sit a hair shorter than it. */
body:not( [data-library-layout="list"] ) .card-footer .indicator
{
	gap: .3rem;
	padding: .18rem .5rem;
	border-radius: 3px;
	background: color-mix( in srgb, var( --ink-quiet ) 9%, transparent );
	color: var( --ink );
	font-weight: 500;
	line-height: 1.3;
	white-space: nowrap;
}

/* The toned ones keep their colour and get the matching pale ground -- the same pairs the badges
   above use, so a "Public" pill here is the "Public" badge on the item page. */
body:not( [data-library-layout="list"] ) .card-footer .indicator[data-tone="accent"]
{
	background: var( --accent-quiet );
	color: var( --accent );
}

body:not( [data-library-layout="list"] ) .card-footer .indicator[data-tone="warning"]
{
	background: var( --warning-quiet );
	color: var( --warning );
}

body:not( [data-library-layout="list"] ) .card-footer .indicator .glyph
{
	font-size: var( --text-xs );
}

body:not( [data-library-layout="list"] ) .card-footer .indicator[data-tone="result"] .glyph
{
	font-size: .45rem;
}

.item-card .card-title
{
	margin: 0;
	/* Room for the star and the kebab, which sit in the body's top right corner beside the first
	   line of the title. On the title alone: the meta lines under it keep the card's full width. */
	padding-right: calc( 2 * var( --icon-button-size ) + .2rem );
	font-size: var( --text-base );
	font-weight: 600;
	line-height: 1.3;
	color: var( --ink );
	overflow: hidden;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
}

.item-card .card-title:hover
{
	text-decoration: underline;
}

/* WHERE THIS ITEM LIVES -- its own row, above the meta line (Jon, 2026-08-30).

   Its own row rather than two more cells on the meta line because it is a different kind of
   statement: the meta line is facts ABOUT the item, and a reader scanning it for a size or a date
   should be able to skip this in one movement rather than read past it. Team then project, with a
   drawn separator so the two names cannot be mistaken for one long one.

   Slightly stronger than the meta line beneath it and still quieter than the title. It is context,
   so it must not compete with the thing it is context FOR -- but a person who is looking for it is
   usually looking at a search result with no idea where it came from, and a whisper would not
   answer them. */
.item-card .card-context
{
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: .2rem .3rem;
	min-width: 0;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	line-height: 1.25;
}

.card-context .context-crumb
{
	min-width: 0;
	color: var( --ink-quiet );
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.card-context .context-crumb:hover
{
	color: var( --ink );
	text-decoration: underline;
}

/* The separator is drawn rather than joined in PHP, for the reason the meta line's is: the renderer
   emits a list of crumbs and the stylesheet decides what goes between them, so a trail of one and a
   trail of two are the same markup.

   `:not( :first-child )` RATHER THAN THE ADJACENT-SIBLING PAIR IT USED TO BE, and the difference is
   the fold. With `+`, a crumb whose previous sibling is the ellipsis button loses its separator --
   which is exactly what happens to the second crumb the moment a person expands the trail and the
   button leaves the flow. Positional works because the renderer emits crumbs and one button, the
   first child is always a crumb, and neither of those is going to stop being true quietly. */
.card-context .context-crumb:not( :first-child )::before,
.card-context .context-more::before
{
	content: "›";
	margin-right: .3rem;
	color: var( --ink-quiet );
}

/* A folded crumb is `hidden` from the server and un-hidden by app.js. Stated rather than left to
   the user-agent sheet: `.card-context` is a flex container, and the day something here sets a
   `display` on a crumb the attribute would quietly stop working. */
.card-context [hidden]
{
	display: none;
}

/* THE ELLIPSIS IS A CONTROL AND MUST NOT LOOK LIKE ONE.

   It sits inside a line of quiet text and its job is to be pressable once somebody has decided they
   want the middle of the path -- so it carries the trail's own colour and size, no border, no
   background, and it only announces itself under the cursor. A button that looked like a button
   here would be the loudest thing on a card whose title is the point. */
.card-context .context-more
{
	padding: 0;
	border: 0;
	background: none;
	color: var( --ink-quiet );
	font: inherit;
	line-height: inherit;
	cursor: pointer;
}

.card-context .context-more:hover
{
	color: var( --ink );
}

/* WHERE THE TRAIL ENDS IN THE FOLDER, THE META LINE STOPS REPEATING IT.

   The Folder cell and the trail's last crumb are the same word, four millimetres apart, and until
   the trail carried folders they never met. The cell stays in the MARKUP because the list layout
   turns it into a column and a row one cell short of its header is the failure viewShowsFolderColumn
   in index.php exists to prevent (D208) -- so this is a stylesheet job, in the layouts where the
   trail is drawn and the columns are not. The order is guaranteed by the card body: the trail is
   emitted above the meta line, and where there is no trail there is no rule.

   `~` RATHER THAN `+`, since the uploader row moved in between the two (2026-09-18). The adjacent
   pair was true for as long as nothing sat between the trail and the meta line, which is exactly the
   kind of silent dependency a later edit breaks -- the folder name quietly came back, twice on one
   card, with nothing failing. The general sibling combinator says what the rule actually means:
   this card HAS a trail, so its meta line need not repeat the folder. */
body:not( [data-library-layout="list"] ) .card-context ~ .card-meta .meta-folder
{
	display: none;
}

.item-card .card-meta
{
	color: var( --ink-quiet );
	font-size: var( --text-xs );
}


/* ---------------------------------------------------------------------------------------------
   THE LIST LAYOUT, AND WHY IT IS A STYLESHEET AND NOT A SECOND RENDERER (D208).

   A person can have this library as a contact sheet or as a table, per view, and the choice lives on
   their account (app/lib/preferences.php). index.php renders ONE markup for both: the same <li>, the
   same five meta cells, the same indicator strip. This block turns that markup ninety degrees.

   Two things follow from that, and both are the reason it was built this way.

   The switch costs nothing. `data-library-layout` is one attribute on <body>, so app.js flips it the
   instant a person presses the control and saves the preference behind their back -- no fetch, no
   re-render, nothing moving under a cursor (ui-design.md §1.2's rule, which a round trip would have
   made impossible to keep).

   And the two layouts cannot drift. A card that grows a field grows a column; there is no second
   function that has to be remembered. What this costs is that the CARD's corners -- the kind badge
   and the uploader chip, both drawn over a preview that a list shrinks to three characters wide --
   are hidden here and shown as columns instead, and vice versa. Both are printed from one row by one
   function, so the duplication is in the markup and never in the decision.

   COLUMN ALIGNMENT WITHOUT A <table>. Every row carries the same `grid-template-columns`, so every
   row's cells land in the same places -- the rows are all the same width, so `fr` resolves
   identically down the page. Reaching the cells needs `display: contents` on the two wrappers
   between the <li> and them; both are a <div> and a <p> with no semantics of their own to lose, and
   neither is interactive, which is the case where `display: contents` is known to be a mistake.

   THE CARD LAYOUT IS THE DEFAULT IN THE STYLESHEET TOO. Every rule here is under
   [data-library-layout="list"], so markup arriving without the attribute at all -- an item page, a
   fragment rendered before the client has caught up -- draws cards. A layout that depended on an
   attribute being present would fail to the wrong thing.
   --------------------------------------------------------------------------------------------- */

/* An empty meta cell renders nothing in the card layout -- an item with no stored original would
   otherwise contribute a stray separator to its meta line.

   **Scoped to the card layout, and that scoping is the whole point of the rule.** `display: none`
   removes a grid item, so the same rule in the list layout would take an empty Size cell out of the
   row and shift every column after it one place left -- on exactly the rows whose original never
   arrived, which are the rows a person is most likely to be reading the table to find. The cell
   still EXISTS in both (index.php on why every row carries all five); here it is merely invisible,
   there it is an empty box holding its column open. */
body:not( [data-library-layout="list"] ) .card-meta .meta-cell:empty
{
	display: none;
}

/* Shown as columns in the list, drawn over the preview in a card. */
.card-meta .meta-list-only
{
	display: none;
}

/* The " · " between the cells of the card's meta line, drawn by the stylesheet rather than joined in
   PHP so that the same five cells can be a line here and a table there. Scoped to the card layout by
   the :not() on <body>, which is also what keeps this rule more specific than the one that cancels
   it below. */
body:not( [data-library-layout="list"] ) .card-meta .meta-cell:not( .meta-list-only ):not( :empty ) ~ .meta-cell:not( .meta-list-only ):not( :empty )::before
{
	content: " · ";
}

/* THE VIEW COUNT (D418) is said twice in the markup and drawn once: a card's meta line reads
   "12 views", the list's Views column reads "12". index.php says why both are there. */
.card-meta .meta-views .views-figure
{
	display: none;
}

/* No columns without a table to head. */
.item-list-head
{
	display: none;
}


[data-library-layout="list"] .item-grid
{
	/* The template, once, as a custom property: the header row and every item row read it, so a
	   column cannot be widened in one and not the other. */
	/* The Kind column is wider than it was: it carries the family AND the file's suffix now
	   ("video MP4"), which is the same pair the card draws over its preview. 4.6rem clipped the
	   suffix on every row, and a truncated extension is worse than none -- "MP" is a format. */
	/* Title is capped (D415, Jon: "'Title' is really loose"): it took every spare rem and pushed the
	   facts a person scans for to the far edge. What width is left over goes to Status now. The feature
	   columns are wide enough for their headings, and horizontal room is not being economised yet. */
	/* Views sits after Duration (D418): 3.6rem holds five figures in tabular numerals. */
	/* State leads the feature columns (D433): wide enough for a dot and a two-word rung. */
	--list-columns: 2.9rem minmax( 9rem, 16rem ) 6.2rem 4.6rem 4.4rem 3.6rem 6.4rem minmax( 5rem, 10rem ) 1.9rem
		minmax( 4.5rem, 7.5rem ) minmax( 5rem, 7rem ) 4.8rem 5.4rem 5.8rem 3.8rem minmax( 6rem, 1fr );

	display: block;
	grid-template-columns: none;
	gap: 0;
	border: 1px solid var( --rule );
	border-radius: .6rem;
	background: var( --card-background );
	overflow: hidden;
}

/* Inside a folder there is no folder column: index.php emits neither the heading nor the cells (see
   viewShowsFolderColumn), so this is only the narrower template. There WAS a `.meta-folder
   { display: none }` here as well, hiding a cell the rows were still emitting -- correct on screen
   and one column out in the markup, which the proof suite caught by counting both. */
[data-library-layout="list"] .item-grid[data-folder-column="no"]
{
	--list-columns: 2.9rem minmax( 9rem, 18rem ) 6.2rem 4.6rem 4.4rem 3.6rem 6.4rem 1.9rem
		minmax( 4.5rem, 7.5rem ) minmax( 5rem, 7rem ) 4.8rem 5.4rem 5.8rem 3.8rem minmax( 6rem, 1fr );
}

[data-library-layout="list"] .item-list-head,
[data-library-layout="list"] .item-card
{
	display: grid;
	grid-template-columns: var( --list-columns );
	/* One row. The card layout's picture-then-words pair of rows is not this layout's. */
	grid-template-rows: none;
	align-items: center;
	column-gap: .7rem;
}

/* The bottom border is transparent and the box is still there (Jon, 2026-09-03, folded in from
   experiments.css): the first row under the header draws its own top rule, so a visible one here was
   two lines a pixel apart. Transparent rather than `border-bottom: 0`, because the header and the
   rows share `--list-columns` and a header one pixel shorter than it used to be would shift every
   column heading up off its column. */
[data-library-layout="list"] .item-list-head
{
	padding: .4rem .7rem;
	border-bottom: 1px solid transparent;
	background: color-mix( in srgb, var( --page-background ) 55%, var( --card-background ) );
	color: var( --ink-quiet );
	font-size: var( --text-2xs );
	font-weight: 600;
	letter-spacing: .05em;
	text-transform: uppercase;
}

[data-library-layout="list"] .item-card
{
	padding: .3rem .7rem;
	border: 0;
	border-top: 1px solid var( --rule );
	border-radius: 0;
	background: transparent;
	box-shadow: none;
}

[data-library-layout="list"] .item-grid > .item-card:first-of-type
{
	border-top: 0;
}

[data-library-layout="list"] .item-card:hover
{
	background: color-mix( in srgb, var( --accent-quiet ) 55%, transparent );
}

/* A removed item keeps saying so. The card layout says it with a dashed border all the way round;
   a row has one border, so it says it with the same dash on that. */
[data-library-layout="list"] .item-card[data-state="removed"]
{
	border-top-style: dashed;
}

/* The three wrappers dissolve so their children become the row's cells. The footer is the third
   (2026-09-22): its legend line is hidden here and its indicator strip is the Status column, exactly
   as when both were children of the body. */
[data-library-layout="list"] .card-body,
[data-library-layout="list"] .card-meta,
[data-library-layout="list"] .card-footer
{
	display: contents;
}

[data-library-layout="list"] .card-meta .meta-list-only
{
	display: block;
}

/* The card's meta separators are a card thing. */
[data-library-layout="list"] .card-meta .meta-cell::before
{
	content: none;
}

[data-library-layout="list"] .item-card .card-preview
{
	width: 2.9rem;
	border-radius: .25rem;
	background-size: .5rem .5rem;
}

/* Everything the card draws OVER its preview. There is no room for any of it on a thumbnail this
   size, and each has a column of its own here -- except the progress pill, whose words are already
   in the indicator strip that becomes the Status column (index.php takes the pill's label FROM that
   indicator, so hiding it here loses nothing at all). */
[data-library-layout="list"] .item-card .card-type,
[data-library-layout="list"] .item-card .card-progress
{
	display: none;
}

/* THE PREVIEW'S STAND-IN, AT COLUMN WIDTH (Jon, 2026-10-11: "Waiting for file" clipped to
   "Waitin", the extension at card size inside a 2.9rem tile). Same reasoning as the rules above,
   drawn the other way around: what identifies the file KEEPS its place in the tile, in small
   letters -- the extension at the smallest step with its card spacing dropped, the link's glyph
   alone -- and only the sentences go, because each is a column's own words already ("Waiting for
   file" is the Status column's) and no sentence survives at this width. Long extensions exist
   even small: the base rule's ellipsis is the cap, and the Kind column carries the full suffix. */
[data-library-layout="list"] .item-card .card-preview .no-preview
{
	padding: .15rem;
	font-size: var( --text-2xs );
}

/* The two word-only variants: "Waiting for file" and "No original stored". No extension to show,
   so the tile stays the quiet checkerboard and the row says the rest. */
[data-library-layout="list"] .item-card .card-preview .no-preview:not( [data-kind] )
{
	display: none;
}

[data-library-layout="list"] .no-preview-extension,
[data-library-layout="list"] .card-link-glyph
{
	font-size: var( --text-2xs );
	letter-spacing: 0;
}

[data-library-layout="list"] .no-preview-noun,
[data-library-layout="list"] .card-link-site
{
	display: none;
}

/* THE UPLOADER ROW IS A CARD THING, and the list keeps the chip it always had.

   Structural first, as with the context row below: `.card-body` is `display: contents` here, so every
   child of it is a cell of the row and this one has no heading -- `display: none` takes it out of the
   grid rather than leaving a cell the header cannot account for. And the list answers the question
   already, in the By column, where the chip alone is right: a column is read downwards, the same few
   people recur, and a name per row would take the width from the title. Which is the whole of Jon's
   distinction on 2026-09-18 -- the name in words on a card met alone, the chip in a list. */
[data-library-layout="list"] .item-card .card-person
{
	display: none;
}

/* THE ALIAS LINE IS A CARD THING TOO, and in the list it was worse than clutter: renderAliasCard()
   injects it as the FIRST child of `.card-body`, `display: contents` makes every child a cell, and
   an alias row's every column landed one heading to the right of its words (seen 2026-10-11, in the
   same look that caught the stand-ins above). Out of the grid entirely, on `.card-person`'s
   reasoning: the link roundel on the tile still says "alias", and the home folder is the Folder
   column's job. */
[data-library-layout="list"] .item-card .card-alias-line
{
	display: none;
}

/* One line, and an ellipsis where it does not fit. The card clamps to two lines with -webkit-box,
   which has to be undone rather than adjusted: a two-line row would make every row two lines tall. */
[data-library-layout="list"] .item-card .card-title
{
	display: block;
	-webkit-line-clamp: none;
	font-size: var( --text-sm );
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

[data-library-layout="list"] .meta-cell
{
	min-width: 0;
	color: var( --ink-quiet );
	font-size: var( --text-xs );
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* THE FEATURE COLUMNS (D415). A dot where the item has the feature, nothing where it does not, and the
   public-video captions warning in the Captions column. Centred under their headings, which are
   centred too, so a column of dots reads as a column. */
[data-library-layout="list"] .meta-feature,
[data-library-layout="list"] .head-captions,
[data-library-layout="list"] .head-transcript,
[data-library-layout="list"] .head-translation,
[data-library-layout="list"] .head-rights
{
	text-align: center;
}

[data-library-layout="list"] .meta-feature .feature-dot
{
	color: var( --ink );
	font-size: var( --text-base );
	line-height: 1;
}

/* Wider than the window scrolls sideways rather than squeezing Status to nothing (D415): the rows are
   grids wider than the list, so the list is what scrolls, header and rows together. */
[data-library-layout="list"] .item-grid
{
	overflow-x: auto;
}

[data-library-layout="list"] .item-list-head,
[data-library-layout="list"] .item-card
{
	min-width: max-content;
}

[data-library-layout="list"] .meta-feature .feature-warning
{
	color: var( --warning );
}

[data-library-layout="list"] .meta-duration,
[data-library-layout="list"] .meta-views
{
	font-variant-numeric: tabular-nums;
}

[data-library-layout="list"] .card-meta .meta-views .views-figure
{
	display: inline;
}

[data-library-layout="list"] .card-meta .meta-views .views-phrase
{
	display: none;
}

[data-library-layout="list"] .meta-privacy .glyph
{
	margin-right: .25rem;
}

[data-library-layout="list"] .meta-privacy[data-tone="accent"]
{
	color: var( --accent );
}

/* What the columns now say, taken out of the Status strip so it is not said twice: the publicity and
   captions indicators, and the transcript area's result badge. The card layout keeps all three. */
[data-library-layout="list"] .indicator[data-name="publicity"],
[data-library-layout="list"] .indicator[data-name="captions"],
[data-library-layout="list"] .indicator[data-name="result"][data-surface="transcript"]
{
	display: none;
}

/* A sortable heading is a link that looks like its heading, with the current order's direction beside
   it. The arrow is drawn by the stylesheet from `data-sort-state`, so the markup carries the state once. */
[data-library-layout="list"] .head-sort
{
	color: inherit;
	text-decoration: none;
	white-space: nowrap;
}

[data-library-layout="list"] .head-sort:hover,
[data-library-layout="list"] .head-sort[data-sort-state="forward"],
[data-library-layout="list"] .head-sort[data-sort-state="reverse"]
{
	color: var( --ink );
}

[data-library-layout="list"] .head-sort[data-sort-state="forward"] .head-sort-mark::after
{
	content: " ▾";
}

[data-library-layout="list"] .head-sort[data-sort-state="reverse"] .head-sort-mark::after
{
	content: " ▴";
}

[data-library-layout="list"] .meta-kind
{
	text-transform: uppercase;
	letter-spacing: .03em;
	font-size: var( --text-2xs );
}

/* The same suffix the card draws in its corner badge, in the column that replaces that badge. The
   rule beside the family stays -- it is what stops "VIDEO MP4" reading as one word -- but the
   badge's padding and backdrop belong to a chip floating over a picture, not to a table cell. */
[data-library-layout="list"] .meta-kind .card-type-suffix
{
	margin-left: .3rem;
	padding-left: .3rem;
	border-left: 1px solid var( --rule );
	color: var( --ink );
	font-weight: 650;
}

/* THE CONTEXT ROW IS A CARD THING, and hiding it here is not a loss.

   Two reasons, and the first is structural: .card-body is `display: contents` in this layout, so
   every child of it becomes a cell of the row -- an extra one that no heading accounts for, which
   is precisely the one-column-out failure the folder cell taught us (viewShowsFolderColumn in
   index.php). `display: none` takes it out of the grid entirely rather than leaving an empty box.

   The second is that the list already answers the question. The Folder column is right there, and
   in a table a person is reading down one column at a time -- where the trail's value on a card is
   that it sits with the item, in a row it would be a third place naming the same tree. */
[data-library-layout="list"] .card-context
{
	display: none;
}

[data-library-layout="list"] .indicators
{
	display: flex;
	flex-wrap: nowrap;
	gap: .25rem;
	min-width: 0;
	margin: 0;
	overflow: hidden;
}

/* The pin is absolutely positioned against the card, so it is out of flow and never a cell. It sits
   at the card's top-right corner, which in a row is the row's right end. */
[data-library-layout="list"] .item-card .pin-button
{
	/* A row has one grid row; the card layout's `grid-row: 2` names a line that does not exist here. */
	grid-row: auto;
	top: 50%;
	right: .25rem;
	transform: translateY( -50% );
}

/* Narrow: the columns that are decoration go, rather than every column getting narrower until none
   of them can be read. Title and when survive because they are what a person scans a list FOR.
   Written as one set of hidden cells and one template, so it does not have to be repeated for the
   folder-column variant -- everything conditional is hidden here anyway. */
@media ( max-width: 62rem )
{
	[data-library-layout="list"] .item-grid,
	[data-library-layout="list"] .item-grid[data-folder-column="no"]
	{
		--list-columns: 2.6rem minmax( 0, 1fr ) 6.4rem;
	}

	[data-library-layout="list"] .meta-kind,
	[data-library-layout="list"] .meta-size,
	[data-library-layout="list"] .meta-duration,
	[data-library-layout="list"] .meta-views,
	[data-library-layout="list"] .meta-folder,
	[data-library-layout="list"] .meta-person,
	[data-library-layout="list"] .meta-privacy,
	[data-library-layout="list"] .meta-feature,
	[data-library-layout="list"] .indicators,
	[data-library-layout="list"] .head-kind,
	[data-library-layout="list"] .head-size,
	[data-library-layout="list"] .head-duration,
	[data-library-layout="list"] .head-views,
	[data-library-layout="list"] .head-folder,
	[data-library-layout="list"] .head-person,
	[data-library-layout="list"] .head-privacy,
	[data-library-layout="list"] .head-captions,
	[data-library-layout="list"] .head-transcript,
	[data-library-layout="list"] .head-translation,
	[data-library-layout="list"] .head-rights,
	[data-library-layout="list"] .head-status
	{
		display: none;
	}
}


/* The toolbar's layout control: two buttons that behave as one, so the pair reads as a state and not
   as two independent actions. */
.layout-control
{
	display: inline-flex;
	margin: 0;
}

.toolbar-controls .layout-control .layout-choice
{
	border-radius: 0;
	display: inline-flex;
	align-items: center;
	--icon-size: .95rem;
}

/* The glyph exists for the phone (D352); on a wide screen the word is the control, as it always was.
   The narrow block further down swaps them. */
.layout-choice .icon
{
	display: none;
}

.toolbar-controls .layout-control .layout-choice:first-child
{
	border-radius: .42rem 0 0 .42rem;
}

.toolbar-controls .layout-control .layout-choice:last-child
{
	border-radius: 0 .42rem .42rem 0;
}

/* Every key after the first overlaps its neighbour by the border's width, so two touching edges draw
   one line. This sat on `:last-child` alone from when there were two keys; with four, the middle
   pair drew a double line between them (Jon, 2026-10-02). */
.toolbar-controls .layout-control .layout-choice + .layout-choice
{
	margin-left: -1px;
}

/* Which one you are in. `aria-pressed` is the state a screen reader is told, so styling off the same
   attribute means the two cannot disagree -- there is no separate `is-current` class to forget.

   The border stays `--rule` (Jon, 2026-09-03, folded in from experiments.css). An accent border on
   the pressed segment plus the accent fill made the control the loudest thing in a toolbar whose job
   is to be ignorable: the fill already says which one you are in, and outlining it as well made a
   preference look like an alert. */
.toolbar-controls .layout-control .layout-choice[aria-pressed="true"]
{
	background: var( --accent-quiet );
	border-color: var( --rule );
	color: var( --ink );
	font-weight: 600;
}


/* ---------------------------------------------------------------------------------------------
   THE PHOTO GRID, AND WHY IT IS A THIRD VALUE OF THE SAME ATTRIBUTE (D272).

   Cards and the list are two densities of one page. This is not: it takes the window. The sidebar,
   the toolbar, the footer, the headings under every picture and the meta line under those -- all of
   it goes, and what is left is the pictures, edge to edge, at whatever size the person dragged the
   slider to. The thing it is for is the question a card grid answers badly and a list answers not at
   all: *which of these forty frames is the one I remember?*

   IT IS STILL ONE MARKUP AND ONE ATTRIBUTE. Everything below is scoped to
   [data-library-layout="grid"] on <body>, exactly as the list layout is scoped to "list", so the
   switch remains what D208 built: a stylesheet flip with nothing moving under a cursor, no re-render,
   no round trip. ops/task22 §2 asserts the markup is byte-identical across the layouts and
   ops/task32 extends that assertion to this one -- so a rule here can hide something, and a renderer
   can never grow a branch.

   WHAT IT DELIBERATELY KEEPS. The view's own <h1>, because hiding the breadcrumb takes away the only
   other thing on screen that says where you are; the scope tiles, because they are navigation and a
   photo wall with no way down into the next folder is a dead end; and the processing badge over a
   tile, because an item that is still working is the one caption a picture cannot carry itself.

   WHAT IT REMOVES AND WHY THAT IS SAFE. The toolbar's filter, sort and layout controls go with the
   toolbar -- which is why the grid draws its own way out (see .grid-controls below) and why Escape
   does the same thing. A person can always get back to the controls; they cannot get lost in here.
   --------------------------------------------------------------------------------------------- */

/* The shell loses its sidebar column outright rather than collapsing it to zero. `grid-template-
   columns: 0` would leave the sidebar in the layout, still painting its border and still tabbable --
   a navigation you cannot see but can still reach with a keyboard is worse than one that is gone. */
[data-library-layout="grid"] .app-shell
{
	grid-template-columns: minmax( 0, 1fr );
	grid-template-areas:
		"content"
		"footer";
}

[data-library-layout="grid"] .app-sidebar,
[data-library-layout="grid"] .app-footer
{
	display: none;
}

/* The toolbar stays in the document and stops taking any room: it is where .grid-controls is
   rendered, and that is on purpose (shell/toolbar.php's renderGridControls() says why -- the exit button's
   claim has to be reissued on every navigation, and only this region is). `overflow: visible` is
   what lets a fixed-position child escape a box of zero height. */
[data-library-layout="grid"] .app-toolbar
{
	/* No container on the wall: the band has no controls to ask, and .grid-controls inside it is
	   `fixed`, which must keep measuring from the viewport. */
	container-type: normal;
	min-height: 0;
	padding: 0;
	border-bottom: 0;
	background: none;
	backdrop-filter: none;
	overflow: visible;
}

[data-library-layout="grid"] .app-toolbar > *:not( .grid-controls )
{
	display: none;
}

[data-library-layout="grid"] .load-strip
{
	top: var( --top-clearance, 0px );
}

[data-library-layout="grid"] .app-main
{
	/* The whole layout, in two lines. --card-track and --card-gap are the variables .item-grid and
	   .scope-grid already read (see the note on .app-main above), so overriding them here re-lays
	   every grid on the view at the size the slider says -- and the folder tiles stay in step with
	   the pictures, which is the property that block was written to guarantee. */
	--card-track: var( --grid-tile-size, var( --grid-tile-default ) );
	--card-gap: var( --grid-tile-gap );

	padding: 0 0 4rem;
	scroll-padding-top: 0;
	background: var( --grid-ground );
	scroll-behavior: auto;
}

/* The place, since the breadcrumb went with the toolbar. Quiet and small: it is a label on a wall,
   not a masthead. */
[data-library-layout="grid"] .view-heading
{
	padding: 1rem 1rem .6rem;
	margin: 0;
}

[data-library-layout="grid"] .view-heading h1
{
	font-size: var( --text-lg );
}

[data-library-layout="grid"] .view-section > h2
{
	padding: .9rem 1rem .35rem;
	margin: 0;
	font-size: var( --text-sm );
	font-weight: 600;
	letter-spacing: .04em;
	text-transform: uppercase;
	color: var( --ink-quiet );
}

/* Prose, the drop zone, the badges beside a heading and the dashboard's FYI rail are all chrome by
   this layout's definition of the word: they explain the view rather than showing it.

   The FYI rail is on this list for a reason worth writing down, because it was found by looking at
   the screen and not by an assertion (CLAUDE.md §2). It used to be a bordered panel drawn whether or
   not there was anything in it -- so on a dashboard with an empty FYI, a quarter of a photo wall was
   a box explaining what an FYI would be. Since 2026-09-17 it is drawn only when something is
   waiting, and it stays on this list anyway: leaving the grid brings it back, and it was never a
   thing this layout was opened to look at.

   `.scope-actions` -- New folder, People and grants, Pin, FYI, Share folder -- goes for the same
   reason the toolbar does: administering a folder is not what this layout was opened for, and Done
   is one press away. Note what does NOT go with it: `.notice` is how this interface explains a
   refusal rather than hiding one (invariant 16), and the only notices that disappear here are the
   ones drawn INSIDE the action row, which are explanations of controls that are themselves gone. */
[data-library-layout="grid"] .view-subtitle,
[data-library-layout="grid"] .fyi-rail,
[data-library-layout="grid"] .scope-actions,
[data-library-layout="grid"] .view-heading .badge
{
	display: none;
}

/* The count beside a section heading takes the heading's own size here. It is set in rem elsewhere,
   so at this size it was half again as big as the words it counts. */
[data-library-layout="grid"] .view-section > h2 .count
{
	font-size: inherit;
	font-weight: 400;
}

/* Scope tiles keep their own gutter -- they carry names, and a name touching the tile beside it is
   unreadable in a way a photograph is not. */
[data-library-layout="grid"] .scope-grid
{
	--card-gap: .5rem;

	padding: 0 1rem .4rem;
}

/* THE TILE. Square, because a wall of mixed aspect ratios is a wall with holes in it, and a contact
   sheet is read by scanning rows -- the SLOT is square, so the rows stay regular whatever the
   pictures are.

   The picture inside it is fitted whole, not cropped: `object-fit: contain` where the card elsewhere
   uses `cover`. The crop was argued from recognition -- a tile is small, fill it -- and the argument
   loses to what a cropped contact sheet actually does to material that is not square. A 16:9 still
   loses a third of its width, a portrait phone video loses its top and bottom, and a wall of them is
   a wall of centre-crops in which the thing being recognised is often the part that was cut. Fitted
   whole, every tile shows the whole frame and the tile's own empty colour fills the rest, which is
   what the letterbox bars are. (Jon, 2026-09-09.)

   The bars are `--grid-tile-empty` and nothing else: `.card-preview`'s checkerboard is already off in
   this layout, and the colour is the one an empty tile has, so a letterboxed picture and a picture
   that has not arrived sit on the same ground. */
[data-library-layout="grid"] .item-card
{
	background: var( --grid-tile-empty );
	border: 0;
	border-radius: 0;
	box-shadow: none;
}

[data-library-layout="grid"] .item-card .card-preview
{
	aspect-ratio: 1;
	background: none;
	width: 100%;
}

[data-library-layout="grid"] .item-card .card-preview img
{
	object-fit: contain;
}

/* AND THE CONTACT SHEET IGNORES THE CROP, which is a decision rather than an oversight (D365,
   deferring to D364 taken the same day). The tile is square and shows the whole frame; the crop
   answers "which 16:9 rectangle means this item", and there is no 16:9 box here to put it in.
   Applying it would letterbox a crop inside a square, which is a smaller picture than the one the
   tile shows now, cropped as well. The window is undone rather than overridden piecemeal, because
   the base rule sets four properties and forgetting one of them is a picture hanging out of its
   tile. */
[data-library-layout="grid"] .item-card .card-preview[data-key-crop] img
{
	inset: 0;
	left: auto;
	top: auto;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* Everything a card says in words. The title, the meta line, the indicator strip, the kind badge and
   the uploader chip are all still IN the markup -- one markup, three layouts -- and all of them are
   captions on a picture that does not need one. */
[data-library-layout="grid"] .card-body,
[data-library-layout="grid"] .card-footer,
[data-library-layout="grid"] .item-card .card-type,
[data-library-layout="grid"] .item-card .card-person,
[data-library-layout="grid"] .item-card .indicators,
[data-library-layout="grid"] .item-card .pin-button,
[data-library-layout="grid"] .item-list-head
{
	display: none;
}

/* Two exceptions, and both earn it by saying something the picture cannot.

   The processing chip stays: an item that is still encoding looks exactly like one that is finished,
   and this is the layout with the fewest other places to notice.

   The extension panel stays for anything with no picture at all -- a document, a dataset, an upload
   that never finished. Scaled down, because the panel is sized in `em` off the card's font and a tile
   is a third of a card. */
[data-library-layout="grid"] .item-card .card-progress
{
	display: inline-flex;
}

[data-library-layout="grid"] .item-card .card-preview .no-preview
{
	font-size: var( --text-2xs );
}

/* A moving picture says so. Drawn by the stylesheet off the attribute library.php put there, so
   there is no second decision about which items have video -- the same attribute the lightbox reads
   to know what to play. */
[data-library-layout="grid"] .item-card[data-preview-video] .card-preview::after
{
	content: "▶";
	position: absolute;
	right: .35rem;
	bottom: .3rem;
	z-index: 1;
	font-size: var( --text-sm );
	line-height: 1;
	color: #fff;
	text-shadow: 0 1px 3px rgba( 0, 0, 0, .8 );
	pointer-events: none;
}

/* Hover and keyboard focus, drawn INSIDE the tile as an inset ring rather than outside it: an
   outline on a tile with a two-pixel gutter paints over its neighbours. */
[data-library-layout="grid"] .item-card:hover .card-preview,
[data-library-layout="grid"] .item-card .card-preview:focus-visible
{
	outline: 2px solid var( --accent );
	outline-offset: -2px;
}

/* The keyboard cursor in the cards and list layouts (D411). keyboard.js focuses a card's preview, and
   the preview's own ring was invisible there -- clipped by the card's rounded overflow in cards, and a
   hairline round a thumbnail the size of a word in the list -- so the arrows walked the listing with
   nothing on screen saying where they were. The ring goes on the CARD instead, in ink rather than an
   accent, because a card already wears two accent outlines that mean something else: the selection
   (inset, the selection accent) and "changed while you were away" (the accent, just outside). Outside
   the card in the cards layout, where the gutter has room for it; inset in the list, whose rows abut. */
[data-library-layout="cards"] .item-card:has( > .card-preview:focus-visible )
{
	outline: 2px solid var( --ink );
	outline-offset: 3px;
}

[data-library-layout="list"] .item-card:has( > .card-preview:focus-visible )
{
	outline: 2px solid var( --ink );
	outline-offset: -2px;
}

[data-library-layout="cards"] .item-card .card-preview:focus-visible,
[data-library-layout="list"] .item-card .card-preview:focus-visible
{
	outline: none;
}

[data-library-layout="grid"] .item-card[data-state="removed"]
{
	opacity: .45;
}


/* ---------------------------------------------------------------------------------------------
   The grid's own two controls: how big, and the way out.

   Fixed to the top right of the window, over the pictures, and quiet until the pointer is near them.
   They are the only chrome in this layout, so they have to be findable without being a bar -- a
   toolbar reintroduced at 60% opacity would be the layout arguing with itself.
   --------------------------------------------------------------------------------------------- */

.grid-controls
{
	display: none;
}

[data-library-layout="grid"] .grid-controls
{
	position: fixed;
	top: .75rem;
	right: 1rem;
	z-index: 30;
	display: flex;
	align-items: center;
	gap: .6rem;
	padding: .35rem .55rem;
	border: 1px solid var( --rule );
	border-radius: 1.2rem;
	background: color-mix( in srgb, var( --grid-ground ) 82%, transparent );
	backdrop-filter: saturate( 180% ) blur( 12px );
	box-shadow: var( --shadow-card );
	opacity: .55;
	transition: opacity .14s ease;
}

/* :focus-within as well as :hover, because the slider and the button are both reachable by keyboard
   and a control that only appears under a pointer is a control a keyboard cannot find. */
[data-library-layout="grid"] .grid-controls:hover,
[data-library-layout="grid"] .grid-controls:focus-within
{
	opacity: 1;
}

.grid-controls form
{
	display: flex;
	align-items: center;
	margin: 0;
}

.grid-controls input[ type="range" ]
{
	width: 7rem;
	accent-color: var( --accent );
	cursor: ew-resize;
}

/* THE ICON SIZE SLIDER (D562): in the toolbar of every laid-out view, shown only in the icon layout,
   so the layout press reveals it with no round trip. The grid slider's look, a little narrower,
   because it shares a row with the filter, the sort and the layout control. */
.icon-size-form
{
	display: none;
	align-items: center;
	margin: 0;
}

[data-library-layout="icons"] .icon-size-form { display: flex; }

.icon-size-form input[ type="range" ]
{
	width: 6rem;
	accent-color: var( --accent );
	cursor: ew-resize;
}

.grid-controls .grid-exit
{
	padding: .2rem .7rem;
	border: 1px solid var( --rule );
	border-radius: .9rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	cursor: pointer;
}

.grid-controls .grid-exit:hover
{
	border-color: var( --accent );
	color: var( --accent );
}

/* The slideshow group (Jon, 2026-09-06), left of the slider and ruled off from it: seconds, Loop,
   Hold first, Start. A fieldset with its browser dressing removed -- the element is right (four
   controls that mean one thing) and its default border and padding are not. Start wears the exit
   button's shape rather than .control-pill, because the cluster is one visual object and its two
   buttons should be one kind of button; the pill vocabulary lives in the shell this layout hides. */
.grid-controls .grid-slideshow
{
	display: flex;
	align-items: center;
	gap: .5rem;
	margin: 0;
	padding: 0 .6rem 0 0;
	border: 0;
	border-right: 1px solid var( --rule );
	min-width: 0;
	font-size: var( --text-sm );
	color: var( --ink );
}

.grid-controls .grid-slideshow label
{
	display: inline-flex;
	align-items: center;
	gap: .25rem;
	white-space: nowrap;
	cursor: pointer;
}

.grid-controls .grid-slideshow input[ type="number" ]
{
	width: 3.4rem;
	padding: .15rem .3rem;
	border: 1px solid var( --rule );
	border-radius: .5rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	text-align: right;
}

.grid-controls .grid-slideshow input[ type="checkbox" ]
{
	margin: 0;
	accent-color: var( --accent );
}

.grid-controls .grid-slideshow-start
{
	padding: .2rem .7rem;
	border: 1px solid var( --rule );
	border-radius: .9rem;
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	cursor: pointer;
}

.grid-controls .grid-slideshow-start:hover
{
	border-color: var( --accent );
	color: var( --accent );
}

/* Narrow screens: the slider is the first thing to go, and the slideshow group with it. A phone has
   no room for either, a photo wall on a phone is already at the size a phone can show, and a
   presentation is not given from one. */
@media ( max-width: 44rem )
{
	[data-library-layout="grid"] .grid-size-form,
	[data-library-layout="grid"] .grid-slideshow
	{
		display: none;
	}
}


/* ---------------------------------------------------------------------------------------------
   THE LIGHTBOX.

   A <dialog> in the top layer, which is why there is no z-index here and no scroll lock: the element
   already does both, and `showModal()` traps focus and returns it to the tile afterwards. app.js
   fills it; this only dresses it.

   The picture is `object-fit: contain` and never `cover` -- the opposite of the tile, and the
   distinction is the whole point of opening one. A tile is for recognising and may crop; a lightbox
   is for looking at and may not.
   --------------------------------------------------------------------------------------------- */

.lightbox
{
	width: 100vw;
	max-width: 100vw;
	height: 100dvh;
	max-height: 100dvh;
	padding: 0;
	border: 0;
	background: var( --lightbox-ground );
	color: var( --lightbox-ink );
	overflow: hidden;
}

.lightbox::backdrop
{
	background: var( --lightbox-ground );
}

.lightbox-stage
{
	position: absolute;
	inset: 0;
	display: grid;
	/* ONE NAMED CELL, AND EVERYTHING IN THE STAGE SITS IN IT. That is what stacks the soft copy and
	   the sharp one without positioning either of them: two grid items in the same area overlap, both
	   are bounded by this box, and the later one in the markup paints on top.

	   It replaced `position: absolute; inset: 3rem 3.5rem` on the upgrade, which was wrong in a way
	   only a rendered page showed. `max-width: 100%` on an absolutely positioned element resolves
	   against its CONTAINING BLOCK -- the dialog -- and not against the inset box, so the sharp copy
	   came out a full window wide while sitting 3.5rem in from the left: it ran off the top of the
	   screen and under the caption. Found by looking at a screenshot, which is CLAUDE.md §2's point
	   about what the assertions cannot reach. */
	grid-template-areas: "picture";
	/* The tracks are pinned to the stage rather than left `auto`, and this is the second half of the
	   same bug. An auto track sizes to its content's max-content -- a 1080 px original -- so the
	   track itself overflowed the stage and `max-height: 100%` on the picture then resolved against
	   the overflowing TRACK and constrained nothing. `minmax( 0, 1fr )` makes the track exactly the
	   stage's content box, which is what the percentage is supposed to be measured against. */
	grid-template-columns: minmax( 0, 1fr );
	grid-template-rows: minmax( 0, 1fr );
	place-items: center;
	padding: 3rem 3.5rem;
}

.lightbox-stage img,
.lightbox-stage video,
.lightbox-stage .lightbox-note
{
	grid-area: picture;
}

/* FILLED, NOT CENTRED AT ITS NATURAL SIZE, and the choice is worth defending because the honest
   alternative looks like a bug. The image chain makes one 640 px thumbnail (Blueprint §8.5), so for
   an item whose original this reader may not have, 640 px is the whole picture -- and a 640 px
   picture drawn at 1:1 in the middle of a 1440 px black field reads as a rendering failure, not as
   restraint. Scaled up it is soft, which is a thing a person can see the reason for.

   `object-fit: contain` is what keeps that from being a crop: the box fills, the picture fits inside
   it whole. This is the opposite of the tile, which crops on purpose -- a tile is for recognising and
   a lightbox is for looking at. */
.lightbox-stage img,
.lightbox-stage video
{
	width: 100%;
	height: 100%;
	object-fit: contain;
	background: #000;
}

/* The soft copy first, the sharp one faded in over it once it has decoded (lightbox.js says why).
   Stacked rather than swapped, so there is never a frame with nothing in it. */
.lightbox-stage .lightbox-upgrade
{
	opacity: 0;
	transition: opacity .18s ease;
}

.lightbox-stage .lightbox-upgrade[data-ready="true"]
{
	opacity: 1;
}

.lightbox-note
{
	max-width: 28rem;
	text-align: center;
	font-size: var( --text-base );
	color: color-mix( in srgb, var( --lightbox-ink ) 70%, transparent );
}

.lightbox-caption
{
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: .8rem;
	padding: .7rem 1rem 1rem;
	font-size: var( --text-sm );
	color: color-mix( in srgb, var( --lightbox-ink ) 78%, transparent );
	background: linear-gradient( to top, rgba( 0, 0, 0, .55 ), transparent );
	pointer-events: none;
}

.lightbox-caption a
{
	color: var( --lightbox-ink );
	pointer-events: auto;
	text-decoration: underline;
}

.lightbox button
{
	position: absolute;
	border: 0;
	border-radius: 50%;
	background: rgba( 255, 255, 255, .1 );
	color: var( --lightbox-ink );
	font-size: var( --text-xl );
	line-height: 1;
	width: 2.6rem;
	height: 2.6rem;
	cursor: pointer;
	transition: background .12s ease;
}

.lightbox button:hover,
.lightbox button:focus-visible
{
	background: rgba( 255, 255, 255, .24 );
}

.lightbox .lightbox-close  { top: 1rem; right: 1rem; }
.lightbox .lightbox-previous { left: .6rem; top: 50%; transform: translateY( -50% ); }
.lightbox .lightbox-next     { right: .6rem; top: 50%; transform: translateY( -50% ); }

/* A step that has nowhere to go is drawn as unavailable rather than removed: an arrow that vanishes
   at the end of a set moves the other one under the cursor. */
.lightbox button[disabled]
{
	opacity: .25;
	cursor: default;
}

@media ( max-width: 44rem )
{
	.lightbox-stage { padding: 2.5rem .5rem 3.5rem; }
}


/* ---------------------------------------------------------------------------------------------
   THE SLIDESHOW: the same dialog with three attributes on it (lightbox.js says what each means).

     data-slideshow   this is the show: pictures edge to edge, stages that fade, chrome that hides
     data-controls    a click asked for the chrome; "true" shows it
     data-idle        the pointer has rested; the cursor goes away with the chrome

   Nothing here is a new surface. It is the lightbox with its padding taken off and its buttons put
   behind an attribute, which is why it needs no new colours and no new z-index.
   --------------------------------------------------------------------------------------------- */

.lightbox[data-slideshow="true"] .lightbox-stage
{
	/* Edge to edge: a presentation has no margin, and the stage's black ground is the screen's. */
	padding: 0;
	/* The fade. Two stages overlap for this long; lightbox.js removes the old one when it is done. */
	transition: opacity .5s ease;
}

.lightbox[data-slideshow="true"] .lightbox-stage[data-entering="true"]
{
	opacity: 0;
}

.lightbox[data-slideshow="true"][data-idle="true"]
{
	cursor: none;
}

/* The dialog itself holds the focus during the show (lightbox.js says why), and a focus ring drawn
   around the whole screen is not information anybody needs. */
.lightbox[data-slideshow="true"]:focus
{
	outline: none;
}

/* The chrome as one thing: hidden until asked for, and faded rather than snapped either way, so a
   bar appearing over a picture arrives the way the pictures do. `pointer-events: none` while hidden
   is what lets a click on the ground reach the stage rather than an invisible button. */
.lightbox[data-slideshow="true"] .slideshow-chrome
{
	opacity: 0;
	pointer-events: none;
	transition: opacity .2s ease;
}

.lightbox[data-slideshow="true"][data-controls="true"] .slideshow-chrome
{
	opacity: 1;
	pointer-events: auto;
}

.slideshow-bar
{
	position: absolute;
	top: 1rem;
	left: 50%;
	transform: translateX( -50% );
	display: flex;
	align-items: center;
	gap: .6rem;
	padding: .35rem .6rem;
	border-radius: 999px;
	background: rgba( 0, 0, 0, .55 );
	font-size: var( --text-sm );
	color: color-mix( in srgb, var( --lightbox-ink ) 85%, transparent );
}

.slideshow-position
{
	padding: 0 .4rem;
	font-variant-numeric: tabular-nums;
}

.lightbox[data-empty="true"] .slideshow-position
{
	display: none;
}

/* Worded buttons in the bar, so the lightbox's round-glyph rule is undone for them: static in the
   bar's flow, a pill, and sized by their words. */
.lightbox .slideshow-bar button
{
	position: static;
	width: auto;
	height: auto;
	padding: .3rem .8rem;
	border-radius: 999px;
	font-size: var( --text-sm );
	line-height: 1.2;
}

@media ( prefers-reduced-motion: reduce )
{
	.lightbox[data-slideshow="true"] .lightbox-stage,
	.lightbox[data-slideshow="true"] .slideshow-chrome
	{
		transition: none;
	}
}


/* ---------------------------------------------------------------------------------------------
   Scope tiles: folders and projects rendered as containers rather than as list rows.
   --------------------------------------------------------------------------------------------- */

.scope-grid
{
	display: grid;
	/* --card-track is set on .app-main so tiles and item cards share one column width; the 14rem
	   fallback is what the sidebar's favourites grid gets, out where that variable is not defined
	   and a 15.5rem minimum would not fit. */
	grid-template-columns: repeat( auto-fill, minmax( var( --card-track, 14rem ), 1fr ) );
	gap: var( --card-gap, .7rem );
	margin: 0;
	padding: 0;
	list-style: none;
}

/* The <li> is the grid item and it stretches to its row; the <a> inside it did not, which is why
   tiles in one row could end up different heights whenever a meta line wrapped. These two
   declarations are the no-JavaScript half of the fix and they handle the common case on their own:
   every tile in a ROW matches. equaliseScopeTiles() in app.js then matches them ACROSS rows.

   `min-width: 0` is the other half, and without it the grid is not a grid (Jon, 2026-09-21: a tile
   overlapping the one beside it). A grid item's `min-width` is `auto`, which resolves to its
   MIN-CONTENT width -- so a name the browser cannot break made the <li> wider than the 15.5rem track
   it was placed in, and it overran its neighbour rather than the track growing to hold it. The
   auto-fill tracks are fixed by then: the row was already divided. Zero lets the item be as narrow
   as its track, and `overflow-wrap` on the text below decides where the name breaks instead. */
.scope-grid > li { display: flex; min-width: 0; }

.scope-tile
{
	flex: 1;
	/* Same reasoning as the <li> above, one level down: the tile is a flex item, its automatic
	   minimum size is its min-content width, and a tile that refuses to shrink puts the text back
	   outside the track. */
	min-width: 0;
	display: flex;
	align-items: center;
	gap: .6rem;
	padding: .65rem .75rem;
	border: 1px solid var( --rule );
	border-radius: .55rem;
	background: var( --card-background );
	color: var( --ink );
}

.scope-tile:hover
{
	text-decoration: none;
	border-color: var( --selection-accent );
}

.scope-tile .tile-glyph
{
	--icon-size: 1.5rem;
	display: flex;
	align-items: center;
	font-size: var( --text-xl );
	color: var( --ink-quiet );
}

.scope-tile .tile-text
{
	min-width: 0;
	/* THE TILE IS A FIXED WIDTH AND A FREE HEIGHT (Jon, 2026-09-21). Every string in it breaks --
	   between words where there are words, and mid-word where there are none, which is what
	   `anywhere` buys over `break-word`: it also lowers the box's MIN-CONTENT width, so the tile can
	   be as narrow as its track however long the unbroken run is. Set on the text block rather than
	   on the name alone because the meta line names a parent, and a project called
	   `Kansainvalinen_mediatuotanto_2026` is exactly as unbreakable as an item called that. */
	overflow-wrap: anywhere;
}

/* The name used to be `white-space: nowrap` with an ellipsis, and the ellipsis NEVER RENDERED: this
   is a <span>, `overflow` does not apply to a non-replaced inline box, and so the two declarations
   that were supposed to contain the name did nothing while the third made it unbreakable. The tile
   grew instead of the text shrinking. Wrapping is also the better answer for this rail: the name is
   the whole reason to click the tile, so the four tiles reading "Inbox" that D452 wrote about would
   be four tiles reading "Inbox" with the distinguishing half cut off. Let it wrap; the row grows. */
.scope-tile .tile-name
{
	font-weight: 600;
	font-size: var( --text-base );
}

.scope-tile .tile-meta
{
	color: var( --ink-quiet );
	font-size: var( --text-xs );
}


/* THE LARGE TILE (D318) -- the front page's Favorites rail. The same .scope-tile a project page draws
   its folders with, drawn bigger: a 3rem glyph slot that holds an item's key image where there is one
   so the picture reads as a picture rather than a smudge, a name allowed a second line, and a meta
   line allowed to wrap because it now places the thing ("A video in Selects") instead of naming its
   kind. Selected by the attribute on the <ul> rather than by a second class on every tile, and the
   sidebar's own favorites grid never carries it, so the compact tile there is untouched. Width is
   still --card-track: the point is that an item tile, a folder tile and the Recent items cards below
   them all sit on one column grid. */
.scope-grid[data-tile-size="large"] .scope-tile
{
	gap: .8rem;
	padding: .8rem .9rem;
}

.scope-grid[data-tile-size="large"] .scope-tile .tile-glyph
{
	--icon-size: 1.9rem;
	flex: none;
	justify-content: center;
	width: 3rem;
	height: 3rem;
	font-size: var( --text-2xl );
}

.scope-grid[data-tile-size="large"] .tile-thumb
{
	display: block;
	width: 3rem;
	height: 3rem;
	object-fit: cover;
	border-radius: .3rem;
	background: var( --rule );
}

/* The crop hint as a FOCAL POINT (D365). A square cannot hold a 16:9 rectangle, so this tile takes
   the one thing from it that a square can use: crop towards the middle of what somebody chose rather
   than towards the middle of the frame. The four window percentages ride along in the same attribute
   and are simply not read here. */
.scope-grid .tile-thumb[data-key-crop]
{
	object-position: var( --key-crop-focus );
}

.scope-grid[data-tile-size="large"] .scope-tile .tile-name
{
	white-space: normal;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	/* `-webkit-line-clamp` sets the BOX to two lines and clips nothing on its own: without this the
	   third and fourth lines are simply painted outside it. It used to arrive from the base
	   `.tile-name` rule, where it was inert (that name is an inline span) and where removing it cost
	   nothing -- except here and in the icon layout, the two places the name is a block and the
	   declaration is load-bearing. Declared in both, now, next to the clamp it belongs to. */
	overflow: hidden;
	line-height: 1.25;
}

.scope-grid[data-tile-size="large"] .scope-tile .tile-meta
{
	white-space: normal;
	margin-top: .15rem;
}

/* "All 14 favorites" under a rail that stopped at twelve. */
.rail-more
{
	margin: .55rem 0 0;
	font-size: var( --text-base );
}


/* A person result borrows the scope tile wholesale (renderPersonTiles(), and its comment says why),
   so the only thing to say here is that the initials chip is decoration inside a link rather than a
   hoverable object of its own -- otherwise the cursor changes to `help` over half of the tile. */
.scope-tile .person-chip
{
	cursor: inherit;
}

/* A tile that is not a link: a System Admin can find somebody they share no place with, and there is
   no page in Buffet to send them to. Same tile, no hover promise. */
.scope-tile[data-person-result]:not( a )
{
	cursor: default;
}


/* ---------------------------------------------------------------------------------------------
   Tables (people, shares, capability matrix).
   --------------------------------------------------------------------------------------------- */

.data-table
{
	width: 100%;
	border-collapse: collapse;
	background: var( --card-background );
	border: 1px solid var( --rule );
	border-radius: .55rem;
	overflow: hidden;
	font-size: var( --text-base );
}

.data-table th,
.data-table td
{
	padding: .45rem .65rem;
	text-align: left;
	vertical-align: top;
	border-top: 1px solid var( --rule );
}

.data-table thead th
{
	border-top: 0;
	font-size: var( --text-xs );
	letter-spacing: .05em;
	text-transform: uppercase;
	color: var( --ink-quiet );
	white-space: nowrap;
}

/* A row lights under the pointer (Jon, 2026-09-05). These tables run to thirty rows of small type,
   and the eye loses its line between a name at the left and a badge at the right; a tint on the
   whole row is the ruler. A few percent of ink over the card, which is a hover and not a selection
   -- --selection-background is for a thing that has been chosen, and this has only been pointed at. */
.data-table tbody tr:hover > td
{
	background: color-mix( in srgb, var( --ink ) 4%, var( --card-background ) );
}

.table-scroll
{
	overflow-x: auto;
}

/* The load-more button under a long table: "Show 20 more", drawn by app.js from rows already in the
   document, and rendered `hidden` for a reader without script, who simply gets the whole table. */
.table-load-more { margin-top: .6rem; }
/* `hidden` on a pill does nothing by itself: .control-pill sets `display`, and an author rule beats
   the browser's own `[hidden] { display: none }`. So a table with nothing more to show drew a button
   reading "Show 0 more" (seen on the People page, 2026-10-05). */
.table-load-more[hidden] { display: none; }


/* ---------------------------------------------------------------------------------------------
   Notices and empty states.
   --------------------------------------------------------------------------------------------- */

.notice
{
	display: flex;
	gap: .6rem;
	padding: .7rem .85rem;
	margin: 0 0 1.25rem;
	border-radius: .5rem;
	background: var( --accent-quiet );
	color: var( --accent );
	font-size: var( --text-base );
	line-height: 1.45;
}

.notice[data-tone="warning"]
{
	background: var( --warning-quiet );
	color: var( --warning );
}

.notice[data-tone="quiet"]
{
	background: color-mix( in srgb, var( --ink-quiet ) 9%, transparent );
	color: var( --ink-quiet );
}

.notice .notice-glyph
{
	flex: 0 0 auto;
}

.empty-state
{
	padding: 2.5rem 1.5rem;
	border: 1px dashed var( --rule );
	border-radius: .6rem;
	color: var( --ink-quiet );
	text-align: center;
	font-size: var( --text-base );
}

/* ---------------------------------------------------------------------------------------------
   Toasts, and the staleness badge. Two surfaces, two jobs (D225).

   Jon's line for the split, and it is the whole design: **the toast gets attention, the badge
   carries the state.** A toast is news about something that finished -- it arrives, it says what,
   it goes away. The badge is a standing fact about the view on screen -- it does not move, it does
   not animate more than a heartbeat, and it stays until the view is refreshed.

   What they replace is one sticky banner IN the flow of the main region, which announced everything
   that happened anywhere with the same eight words and pushed the grid down the page to do it.
   --------------------------------------------------------------------------------------------- */

/* Absolute rather than fixed, so the host belongs to the content column and cannot drift over the
   sidebar when the viewport narrows. Nothing here is in the flow, which is the point: an
   announcement must never move the thing it is announcing. */
.app-content
{
	position: relative;
}

.toast-host
{
	position: absolute;
	top: calc( var( --toolbar-height ) + .6rem );
	right: 1rem;
	z-index: 40;

	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: .4rem;

	width: min( 22rem, calc( 100% - 2rem ) );

	/* The host spans a column of empty space most of the time, and a transparent element that eats
	   clicks over the top-right corner of every grid would be a bug nobody could see. Each toast
	   turns pointer events back on for itself. */
	pointer-events: none;
}

.toast
{
	pointer-events: auto;

	display: flex;
	align-items: flex-start;
	gap: .5rem;
	width: 100%;

	padding: .5rem .55rem .5rem .7rem;
	border: 1px solid var( --rule );
	border-left: 3px solid var( --accent );
	border-radius: .45rem;
	background: var( --card-background );
	box-shadow: var( --shadow-card );
	font-size: var( --text-sm );

	animation: toast-arrive .18s ease-out;
}

.toast[data-tone="warning"]
{
	border-left-color: var( --warning );
}

/* The one toast that is not news and does not time out: what moved while somebody was away. It
   holds until a deliberate click or key, because §1.2 makes surviving the return to activity a
   condition of the automatic refresh existing at all. */
.toast[data-kind="applied"]
{
	background: var( --accent-quiet );
	border-color: color-mix( in srgb, var( --accent ) 35%, var( --rule ) );
}

.toast-body
{
	flex: 1 1 auto;
	min-width: 0;
	color: var( --ink );
	line-height: 1.4;
}

a.toast-body:hover
{
	color: var( --accent );
	text-decoration: none;
}

.toast-title
{
	font-weight: 600;
	/* One line, because a 200-character filename is a legitimate thing to have uploaded and this is
	   a notification rather than a document. The detail below it wraps freely. */
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.toast-detail
{
	color: var( --ink-quiet );
}

.toast-dismiss
{
	flex: 0 0 auto;
	border: 0;
	border-radius: .3rem;
	padding: 0;
	min-width: var( --icon-button-size );
	min-height: var( --icon-button-size );
	background: transparent;
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --icon-button-glyph );
	line-height: 1;
	cursor: pointer;
}

.toast-dismiss:hover
{
	background: var( --selection-background );
	color: var( --ink );
}

@keyframes toast-arrive
{
	from { opacity: 0; transform: translateY( -.35rem ); }
	to   { opacity: 1; transform: none; }
}


/* The staleness badge, in the toolbar beside the breadcrumb it is a statement about. Badge-sized
   and quiet on purpose: it is not asking to be pressed, it is recording that the view has moved on
   without this page. Pressing it applies the update, which is what the old banner's button did.

   **Quiet, but no longer inaudible** (Jon, this session). A .4rem dot fading in a toolbar nobody is
   looking at was quiet to the point of being decorative -- the badge was doing the job of a banner
   with the volume of a full stop. The pill itself now carries a halo that expands and fades, so the
   thing that moves is badge-sized rather than dot-sized. It is still the quiet half of the pair
   D225 describes: the toast still gets the attention and goes away, this still carries the state
   and stays. */
.toolbar-stale
{
	display: inline-flex;
	align-items: center;
	gap: .35rem;

	border: 1px solid color-mix( in srgb, var( --accent ) 45%, var( --rule ) );
	border-radius: 999px;
	padding: .15rem .6rem .15rem .5rem;

	/* **INK ON A TINT, NOT ACCENT ON --accent-quiet, AND THE REASON IS MEASURED** (Jon, 2026-09-05:
	   *"its also quite hard to read"*).

	   The old pair was `--accent` text on `--accent-quiet`, which is legible only while both halves
	   come from one palette. They do not have to: experiments.css derives `--accent-quiet` as
	   `color-mix( var( --accent ) 12%, #ffffff )` -- mixed into WHITE unconditionally, with no dark
	   branch -- so in the dark theme this badge was cyan text on a near-white lozenge. That file's
	   own comment already carries the number: #1a9fc1 is **3.10:1 on white**, where AA wants 4.5:1
	   for text this size. It was a measured failure, not a matter of taste, and going to a FILLED
	   accent pill would not have fixed it either -- white on that same cyan is the same 3.10:1.

	   So the badge stops asking the accent to be legible and asks it only to be recognisable.
	   `--ink` on a 14% accent tint of `--card-background` is guaranteed by every palette in both
	   directions, because ink-on-card IS each theme's primary text pair. Computed against this
	   file's own palettes with experiments.css's cyan as the accent: **15.4:1** in the light theme
	   (#16181c on #dff2f6) and **11.4:1** in the dark one (#eceef1 on #1e333a). Neither can be
	   broken by an accent override, because neither half of the pair is the accent. The accent
	   identity survives where it costs nothing to read -- the dot, the border and the halo.

	   .78rem rather than .72rem because that is this project's size for an accent-toned pill label,
	   and 600 because a badge is a label rather than prose. Still the quiet half of D225's pair: the
	   toast is what interrupts, this is what stays -- it is now merely readable while it stays. */
	background: color-mix( in srgb, var( --accent ) 14%, var( --card-background ) );
	color: var( --ink );
	font: inherit;
	font-size: var( --text-sm );
	font-weight: 600;
	white-space: nowrap;
	cursor: pointer;

	animation: attention-halo var( --attention-rhythm ) ease-out infinite;
}

/* The halo and the dot are ONE pulse rather than two things blinking, which is why both take the
   shared tempo and neither states a duration of its own. A ring that grows out of the pill and
   dissolves, rather than a glow that sits there: expansion is what the eye catches in peripheral
   vision, and it leaves nothing behind between beats, so the resting appearance of the toolbar is
   unchanged.

   **THE MIDDLE KEYFRAME IS THE WHOLE OF WHY THIS WAS INVISIBLE**, and it is worth stating because
   the shape is a common one to get wrong. The first version had two stops: full alpha at zero
   spread, then zero alpha at full spread. Every frame in between traded size against opacity, so
   the largest ring was completely transparent and the only opaque ring had no size -- the ring was
   never simultaneously big enough and solid enough to see. Jon's reading of it was *"I'm not sure
   the updates button animates"*, which was almost right: it did, with nothing to show.

   Holding alpha through the growth is the fix. At 40% of the cycle the ring is .3rem of real,
   30%-opacity accent, which is a ring; only after that is it allowed to dissolve. */
@keyframes attention-halo
{
	0%   { box-shadow: 0 0 0 0     color-mix( in srgb, var( --accent ) 60%, transparent ); }
	40%  { box-shadow: 0 0 0 .3rem color-mix( in srgb, var( --accent ) 30%, transparent ); }
	75%  { box-shadow: 0 0 0 .55rem color-mix( in srgb, var( --accent ) 0%, transparent ); }
	100% { box-shadow: 0 0 0 0     color-mix( in srgb, var( --accent ) 0%, transparent ); }
}

.toolbar-stale::before
{
	content: "";
	width: .45rem;
	height: .45rem;
	border-radius: 50%;
	/* The accent, explicitly, and no longer `currentColor` -- the text above went to `--ink`, and
	   inheriting it would have quietly turned this dot the same colour as the words beside it. The
	   dot is now the only place the accent hue appears at full strength, which is what keeps the
	   badge recognisably an accent object. */
	background: var( --accent );

	/* TWO channels on one beat. `attention-pulse` is the shared opacity swing every other indicator
	   in the interface uses, unchanged; the swell is added on top because this dot is 7px of chrome
	   at the top of the screen, where the footer's dots sit inside a line of text a person is
	   already reading. At that size an opacity dip is below the threshold of being noticed at all,
	   and something Jon could not tell was moving is not an indicator. Different properties, so the
	   two compose rather than one overriding the other, and both take the shared tempo -- the beat
	   is still one beat. */
	animation:
		attention-pulse var( --attention-rhythm ) ease-in-out infinite,
		attention-swell var( --attention-rhythm ) ease-in-out infinite;
}

@keyframes attention-swell
{
	0%, 100% { transform: scale( 1 ); }
	50%      { transform: scale( 1.6 ); }
}

.toolbar-stale[hidden]
{
	display: none;
}

/* §17 treats this as an obligation rather than a preference, and a blinking dot is exactly the
   thing it is about. The badge keeps its whole meaning without the heartbeat. */
@media ( prefers-reduced-motion: reduce )
{
	.toast
	{
		animation: none;
	}

	.toolbar-stale,
	.toolbar-stale::before
	{
		animation: none;
	}
}


/* ---------------------------------------------------------------------------------------------
   Footer: the status bar.
   --------------------------------------------------------------------------------------------- */

.app-footer
{
	grid-area: footer;
	display: flex;
	align-items: center;
	/* .5rem rather than the 1rem it was: the tray buttons carry their own padding and border now, so
	   the old gap between bare words would put a hand's width between two adjacent buttons. */
	gap: .5rem;
	min-height: var( --footer-height );
	padding: 0 .75rem;
	border-top: 1px solid var( --rule );
	background: var( --sidebar-background );
	color: var( --ink-quiet );
	font-size: var( --text-xs );
}

.app-footer .status-group
{
	display: flex;
	align-items: center;
	gap: .35rem;
	white-space: nowrap;
}

/* The diagnostics button is rendered `hidden` and unhidden by the footer poll only when PHP actually
   raised something, so this rule is what keeps an empty button out of the corner on every ordinary
   page: an author `display` beats the user-agent's `[hidden] { display: none }` regardless of
   specificity, and the declaration above is one. Same defect as the selection bar's, found in the
   same sweep. */
.app-footer .status-group[hidden]
{
	display: none;
}

.app-footer .status-group[data-tone="accent"]
{
	color: var( --accent );
}

.app-footer .status-group[data-tone="warning"]
{
	color: var( --warning );
}

.app-footer .status-spacer
{
	flex: 1;
}

/* The daily tip (D641) rides in the spacer, centred in the room the spacer already took. One line,
   cut with an ellipsis rather than wrapped: the footer is a fixed height and a second line would grow
   it under every page. The file caps a tip at 280 characters; the ellipsis is for a narrow window.
   `min-width: 0` is what lets a flex child shrink below its text at all. */
.app-footer .daily-tip-slot
{
	display: flex;
	justify-content: center;
	min-width: 0;
}

.app-footer .daily-tip
{
	display: flex;
	align-items: center;
	gap: .35rem;
	min-width: 0;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
}

.app-footer .daily-tip-label
{
	flex: none;
	font-weight: 600;
}

.app-footer .daily-tip-text
{
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.app-footer .daily-tip-text a { color: inherit; }

.app-footer .daily-tip-dismiss { flex: none; margin: 0; }

.app-footer .daily-tip-dismiss-button
{
	min-width: 1.75rem;
	min-height: 1.75rem;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: inherit;
	font: inherit;
	font-size: var( --text-md );
	line-height: 1;
	cursor: pointer;
}

.app-footer .daily-tip-dismiss-button:hover,
.app-footer .daily-tip-dismiss-button:focus-visible { background: color-mix( in srgb, var( --ink ) 10%, transparent ); }

/* A phone's footer is the trays and nothing else; a tip there would be an ellipsis and a cross. */
@media ( max-width: 46rem )
{
	.app-footer .daily-tip { display: none; }
}

.app-footer .status-dot
{
	width: .5rem;
	height: .5rem;
	border-radius: 50%;
	background: currentColor;
}

/* The shared beat, not a tempo of its own. This dot and the upload tray's bar are two readings of
   one fact -- bytes are moving -- and they sit within a few centimetres of each other, so when they
   breathed at 1.4s and the toolbar's badge at 2.4s the corner shimmered instead of signalling. */
.app-footer .status-dot[data-pulse="true"]
{
	animation: attention-pulse var( --attention-rhythm ) ease-in-out infinite;
}

@media ( prefers-reduced-motion: reduce )
{
	.app-footer .status-dot[data-pulse="true"] { animation: none; }
}

/* "Something in here stopped without finishing", said INSIDE the button that opens the list of it
   rather than in a span standing next to that button. It is a pill and not just coloured text
   because the button it rides in is already coloured by the same fact -- the pill is what makes the
   count legible against a control that has itself gone warning-toned.

   A warning glyph and a count since 2026-09-11, where it used to say "N needing attention" in
   words (Jon): the phrase is the tooltip and the spoken text now, and the footer's width goes to
   the third tray button that is coming. */
.app-footer .status-attention
{
	display: inline-flex;
	align-items: center;
	gap: .2rem;
	border-radius: 999px;
	padding: .05rem .42rem .05rem .32rem;
	background: color-mix( in srgb, var( --warning ) 16%, transparent );
	color: var( --warning );
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	--icon-size: .85rem;
}

.app-footer .status-attention[hidden] { display: none; }


/* ---------------------------------------------------------------------------------------------
   Narrow screens. The sidebar becomes a BOTTOM SHEET rather than disappearing: on a phone the
   navigation is still the whole point, it just cannot be permanent.

   It was a left-edge drawer until 2026-09-07 (D352, Jon), and it moved for two reasons that are both
   about a hand. A drawer opened from a toolbar button at the top left is the far corner of a phone
   held in one hand, and the drawer's own edge -- the thing you swipe to dismiss it -- is the edge iOS
   already reads as swipe-for-back, so every dismissal was a coin toss between closing the menu and
   leaving the page. A sheet from the bottom, opened by a floater at the bottom right, is where a
   thumb already is and touches no edge the platform owns. There is deliberately NO swipe gesture on
   the sheet: the scrim and the floater are the two dismissals, and both are taps.

   The drawer also had a defect the sheet cannot have: it slid over its own toggle, so once it was
   open the only way out was to navigate. The floater sits ABOVE the sheet (z-index) and swaps its
   glyph, so the control that opened it is the control that closes it.

   Three controls exist only in this block -- the floater, the scrim and the sheet -- and the base
   rules ABOVE the block keep the first two `display: none` at every other width. Source order is
   the whole mechanism: the block's rules have the same specificity as the base rules, so they win
   only by coming later, and the grid-layout rule that hides the floater outranks both by carrying
   the <body> attribute. (The first draft of this put the base rules after the block, and the phone
   showed neither a floater nor a scrim -- found by looking at the screen, CLAUDE.md §2.)
   --------------------------------------------------------------------------------------------- */

/* The base state of the three narrow-screen controls: absent. Written BEFORE the block below on
   purpose -- see its header -- so the block's rules, at the same specificity, come later and win. */
.sidebar-toggle
{
	display: none;
	align-items: center;
	justify-content: center;
	border: 1px solid var( --rule );
	background: var( --card-background );
	color: var( --ink );
	font: inherit;
	line-height: 1;
	padding: 0;
	cursor: pointer;
}

.sidebar-scrim
{
	display: none;
}

/* The camera floater (shell/toolbar.php's renderCameraFloater(), 2026-09-17): the navigation floater's twin
   on the LEFT, drawn on every page since D446 and only in the block below. Base state absent, before
   the block, for the same source-order reason as the two rules above. */
.camera-floater
{
	display: none;
	align-items: center;
	justify-content: center;
	border: 1px solid var( --rule );
	background: var( --card-background );
	color: var( --ink );
	cursor: pointer;
	--icon-size: 1.35rem;
}

/* The main region's copy of the breadcrumb (index.php's renderMainRegion says why there are two).
   Absent on a wide screen, where the toolbar's copy is the one beside the controls; the narrow block
   below shows it. Wraps, unlike the toolbar's, because here it has a column and not a strip. */
.app-main .view-breadcrumb
{
	display: none;
	flex-wrap: wrap;
	row-gap: .1rem;
	margin: 0 0 .5rem;
}

/* The clone stops at the PARENT (the <h1> beneath it is the current page, and saying it twice a line
   apart was the thing Jon struck), so its last crumb is not the place you are and must not be drawn
   as one; the trailing separator says the title is the next step down. */
.app-main .view-breadcrumb li:last-child a
{
	color: var( --ink-quiet );
	font-weight: 400;
}

.app-main .view-breadcrumb li:last-child::after
{
	content: "›";
	color: var( --ink-quiet );
}

@media ( max-width: 46rem )
{
	.app-shell
	{
		grid-template-columns: minmax( 0, 1fr );
		grid-template-areas:
			"content"
			"footer";
	}

	.app-sidebar
	{
		position: fixed;
		inset: auto 0 0 0;
		/* `auto`, not the clamped preference: a sheet spans the screen, and the width a person dragged
		   the column to on a desk means nothing on a phone. The resizer is hidden below for the same
		   reason. */
		width: auto;

		/* THE SHEET MUST NOT OUTGROW THE VIEWPORT, which is the one hard rule here: a sheet taller
		   than the screen has no scrim left above it, so there is nothing left to tap to dismiss it --
		   it stops being a sheet and becomes a trap. 85 leaves a band of the page visible as the exit.
		   `dvh` where the browser has it, so the sheet is measured against the viewport Safari actually
		   shows once its own bars have collapsed; `vh` is the fallback line. */
		max-height: 85vh;
		max-height: 85dvh;

		/* The reserved band at the bottom is where the floater sits when the sheet is open: the last
		   rows of the tree -- Settings, Sign out -- scroll clear of it rather than under it. The
		   safe-area inset is the home indicator on a notched iPhone, which otherwise sits over the
		   same rows. */
		padding-bottom: calc( 4.25rem + env( safe-area-inset-bottom ) );

		border-right: 0;
		border-top: 1px solid var( --rule );
		border-radius: 1rem 1rem 0 0;
		z-index: 20;
		transform: translateY( 100% );
		box-shadow: 0 -.5rem 2rem rgba( 0, 0, 0, .28 );

		/* `visibility` as well as the transform: a sheet parked below the screen is still in the tab
		   order and still read by a screen reader, and the old drawer had exactly that hole -- a
		   keyboard user could Tab into navigation they could not see. The visibility step is delayed
		   until the slide has finished on the way out, and is immediate on the way in, so the sheet is
		   never invisible while it is on screen. */
		visibility: hidden;
		transition: transform .22s ease, visibility 0s linear .22s;
	}

	body[data-sidebar="open"] .app-sidebar
	{
		transform: none;
		visibility: visible;
		transition: transform .22s ease, visibility 0s;
	}

	/* The dimmed page behind the sheet. It fades with the sheet, and while it is closed it is
	   `display: none` -- NOT merely transparent. This block used to keep it `display: block` at
	   opacity 0 so that opacity alone could transition, and Safari on both iOS and macOS then painted
	   its chrome (the notch and clock strip, the tab and toolbar strip) in the scrim's colour after
	   the sheet was dismissed, because a fixed full-viewport element at opacity 0 is still the
	   topmost thing under the window's edge when Safari samples the page for its tint. A reload
	   cleared it; nothing else did. Transitioning `display` itself (`transition-behavior:
	   allow-discrete`, so the element stays in the tree until the fade-out ends) with
	   `@starting-style` for the fade-in keeps the fade in both directions and takes the element out
	   of the render tree between uses. A browser without discrete transitions ignores the
	   `transition-behavior` line and gets a hard show/hide, which is the correct fallback: the
	   function survives and only the fade is lost. `pointer-events: none` on the closed rule
	   still matters, since during the fade-out the scrim is displayed and must take no tap that
	   was meant for the page. */
	.sidebar-scrim
	{
		display: none;
		position: fixed;
		inset: 0;
		z-index: 19;
		background: rgba( 0, 0, 0, .38 );
		opacity: 0;
		pointer-events: none;
		transition: opacity .22s ease, display .22s ease;
		transition-behavior: allow-discrete;
	}

	body[data-sidebar="open"] .sidebar-scrim
	{
		display: block;
		opacity: 1;
		pointer-events: auto;
	}

	/* The fade-in has to start from somewhere, and an element that was `display: none` a frame ago
	   has no previous opacity to start from; this supplies it. */
	@starting-style
	{
		body[data-sidebar="open"] .sidebar-scrim
		{
			opacity: 0;
		}
	}

	/* The floater. Above the sheet, so it doubles as its close button; above the footer, so the
	   status bar stays readable; and inset from the right by a thumb's width rather than glued to the
	   corner, where a case or a curved screen edge would hide half of it. */
	.sidebar-toggle
	{
		display: inline-flex;
		position: fixed;
		right: 1rem;
		bottom: calc( var( --footer-height ) + 1rem + env( safe-area-inset-bottom ) );
		z-index: 21;
		width: 3.1rem;
		height: 3.1rem;
		border-radius: 50%;
		box-shadow: 0 .25rem 1rem rgba( 0, 0, 0, .28 );
		font-size: var( --text-xl );
	}

	/* The camera, mirrored: same size, height and shadow, inset from the LEFT by the same thumb's
	   width. The left edge is iOS's swipe-for-back, which is why D352 kept the menu off it -- a tap
	   target a rem in from the edge is not a swipe, so a button there is fine where a drawer was not.
	   Below the scrim (19) rather than above the sheet like the menu floater: while the navigation
	   is open, the camera is part of the page it covers. */
	.camera-floater
	{
		display: inline-flex;
		position: fixed;
		left: 1rem;
		bottom: calc( var( --footer-height ) + 1rem + env( safe-area-inset-bottom ) );
		z-index: 18;
		width: 3.1rem;
		height: 3.1rem;
		border-radius: 50%;
		box-shadow: 0 .25rem 1rem rgba( 0, 0, 0, .28 );
	}

	/* Nothing to resize: the sheet's width is the viewport's, and a handle on the edge of a panel that
	   slides away would be a control fighting the tap that dismisses it. */
	.sidebar-resizer
	{
		display: none;
	}

	/* THE BREADCRUMB SWAPS HOMES (D352). The toolbar's copy loses to the kind, sort and layout
	   controls at this width -- what survived was a few ellipses -- so it is hidden and the copy the
	   main region renders at its head takes over, with the whole column to wrap into. The toolbar
	   keeps the controls, which is what it had room for all along. */
	.app-toolbar .breadcrumb
	{
		display: none;
	}

	.app-main .view-breadcrumb
	{
		display: flex;
	}

	/* ONE ROW (Jon, 2026-09-07). Even without the breadcrumb, two selects and the words "Cards",
	   "List" and "Grid" were wider than a phone and the Grid choice sat off the right edge. The
	   layout control collapses to its three glyphs: the word stays in the button, clipped to a pixel
	   the way .visually-hidden does it, so the accessible name is unchanged and `aria-pressed` still
	   says which one you are in. The gutter tightens by a quarter rem each side for the same row. */
	.app-toolbar
	{
		padding: 0 .75rem;
		gap: .5rem;
	}

	/* The mark takes the place the breadcrumb just vacated, at the head of the row: the one width
	   where the wordmark is behind a sheet is the one width where a way home has to be visible. */
	.toolbar-mark
	{
		display: flex;
		align-items: center;
	}
}


/* A TOOLBAR SHORT OF ROOM, HOWEVER IT GOT SHORT (Jon, 2026-10-02). These rules were the phone block's,
   keyed to the viewport; but the toolbar's width is the window LESS the sidebar, and the sidebar is
   dragged to any width (.sidebar-resizer), so a desktop window could hold a toolbar narrower than a
   phone's with the words still in it, and Upload went off the right edge. The toolbar is the
   container now (.app-toolbar) and the question is asked of it: under 55rem -- the words-and-selects
   row is ~700px before the breadcrumb gets any room -- the selects become glyphs and the layout keys
   lose their words, at any window width. A phone's toolbar is always under it, so the phone is
   unchanged; what stayed in the media block above is what is about the phone LAYOUT (the breadcrumb
   moving into the page, the square mark, the tighter gutter). */
@container toolbar ( max-width: 55rem )
{
	.layout-choice .icon
	{
		display: block;
	}

	/* The toolbar gives every button a 4rem floor so that a one-word button is not a sliver; a glyph
	   is not a word, and at the floor three of them were a row of their own -- which the glyphs exist
	   to prevent. `position: relative` anchors the clipped word to its own button rather than to the
	   page. */
	.toolbar-controls .layout-control .layout-choice
	{
		min-width: 0;
		position: relative;
	}

	.layout-choice .layout-choice-label
	{
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset( 50% );
		white-space: nowrap;
	}

	/* AND THE TWO SELECTS BECOME GLYPHS (D430, Jon 2026-09-18). The row above held on a desktop
	   window dragged narrow and failed on the phone it was written for: a touch screen floors every
	   select at 16px (the `pointer: coarse` block at the end of this file, which is right and stays),
	   and two selects at 16px pushed the layout control onto a second row.

	   The wrapper is drawn as the button -- a funnel, a pair of arrows -- and THE SELECT LIES OVER IT
	   AT OPACITY 0. So a tap lands on a real <select> and opens the phone's own picker, which is the
	   best list control a phone has; the label, the keyboard, `data-view-param` and the sort memory
	   are untouched; and there is no script to fall out of step. It keeps its 16px while invisible,
	   which is what stops iOS zooming the page when it takes focus.

	   A glyph cannot show its value. For the sort that is affordable -- the order of the page is
	   the value. For the kind filter it is not: a filtered folder looks like a folder with less in
	   it. So index.php marks the wrapper while the filter is narrowing, and the funnel lights. */
	.toolbar-select
	{
		position: relative;
		display: grid;
		place-items: center;
		flex: none;
		width: 2.6rem;
		height: 2.1rem;
		border: 1px solid var( --rule );
		border-radius: .42rem;
		background: var( --card-background );
		color: var( --ink );
	}

	.toolbar-select-glyph
	{
		--icon-size: 1.15rem;
		display: grid;
		pointer-events: none;
	}

	.toolbar-controls .toolbar-select select
	{
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		min-width: 0;
		min-height: 0;
		padding: 0;
		border: 0;
		opacity: 0;
	}

	.toolbar-select:focus-within { outline: 2px solid var( --accent ); outline-offset: 1px; }
	.toolbar-select:has( select:disabled ) { opacity: .45; }

	.toolbar-select[data-narrowing="true"]
	{
		border-color: var( --accent );
		background: var( --accent-quiet );
		color: var( --accent );
	}

	/* The three layout glyphs grow to the same height, so the strip is one row of five equal keys. */
	.toolbar-controls .layout-control .layout-choice
	{
		height: 2.1rem;
		min-height: 2.1rem;
		min-width: 2.6rem;
	}

	.toolbar-controls .layout-choice .icon { --icon-size: 1.15rem; }

	/* The fallback for a screen narrower than the row even so: the controls wrap rather than run off
	   the edge. At a phone's width this rule does nothing. */
	.toolbar-controls
	{
		flex-wrap: wrap;
		justify-content: flex-end;
		row-gap: .35rem;
	}
}

/* THE SMALLEST PHONES (Jon, 2026-10-02, at the iPhone SE's 375px). The row above needs 419px there --
   the mark, two select glyphs, four layout keys and Upload at 70px -- so Upload wrapped onto a row of
   its own, which also breaks the glass toolbar's fixed overlap (.app-content). Upload alone cannot
   give 44px back and keep its word, so every key gives a little: 2.6rem to 2.1rem, which makes them
   squares at the strip's own height, and with the gaps and Upload's side padding trimmed the row is
   ~357px -- one row down to a 360px Android as well. */
@media ( max-width: 26rem )
{
	.app-toolbar { gap: .35rem; }
	.toolbar-controls { gap: .3rem; }

	.toolbar-select,
	.toolbar-controls .layout-control .layout-choice
	{
		width: 2.1rem;
		min-width: 2.1rem;
	}

	.toolbar-upload
	{
		padding-inline: .45rem;
		min-width: 0;
	}
}

/* One glyph at a time: the hamburger while the sheet is closed, a cross while it is open. The state
   lives on <body>, where sidebar.js writes it, so the swap needs no script of its own. */
.sidebar-toggle .toggle-glyph-close,
body[data-sidebar="open"] .sidebar-toggle .toggle-glyph-open
{
	display: none;
}

body[data-sidebar="open"] .sidebar-toggle .toggle-glyph-close
{
	display: inline;
}

/* The photo wall keeps the <h1> as its only label (see "The place" above), and the floater would
   open a navigation the grid layout has hidden outright. Both go, at every width. */
[data-library-layout="grid"] .sidebar-toggle,
body[data-library-layout="grid"] .sidebar-scrim,
[data-library-layout="grid"] .app-main .view-breadcrumb
{
	display: none;
}

/* The scrim's selector above names <body> where its neighbours do not, and that is not a slip: the
   open rule in the sheet block is `body[data-sidebar="open"] .sidebar-scrim`, which since the
   scrim gained `display: block` on open would outweigh a two-attribute selector and show the scrim
   over the photo wall when a drag reveals the sheet on a narrow screen (mover.js). Matching its
   specificity and coming later in the file keeps the wall's rule the winner. */

@media ( prefers-reduced-motion: reduce )
{
	.app-sidebar,
	.sidebar-scrim
	{
		transition: none;
	}
}


/* ---------------------------------------------------------------------------------------------
   THE SAFE AREA (Jon, 2026-09-20). Installed to an iPhone's home screen (manifest.php asks for
   `display: standalone`) the shell sometimes drew with its logo and toolbar UNDER the status bar,
   where iOS lays its own blur over whatever is beneath -- the LIQUID GLASS status bar of iOS 26,
   which extends a background blur DOWN from the top of the screen (Jon's naming, on the device).
   A band of fog across the head of the page that no rule in this file had drawn, found by Jon on
   the phone and confirmed by pushing <body> down by hand. Without `viewport-fit=cover` iOS decides for itself whether the page is inset from
   the status bar, and "on occasion" it was not, while `env( safe-area-inset-top )` went on saying 0.

   So index.php now asks for `viewport-fit=cover`, which makes it one case instead of two: the page
   ALWAYS reaches the screen's edges, the insets are always true, and the shell pads by them. They
   are 0px in a desktop browser and in any window with no notch, so every rule here is a no-op there.

   The padding goes on the REGIONS and not on the shell, so that each region's own ground runs under
   the status bar and the home indicator, the way a native bar's does; padding the shell would leave
   a strip of page colour above a toolbar tinted differently.

   After the narrow block ON PURPOSE, and with <body> in the selector: that block sets the toolbar's
   padding with a shorthand, which would reset a top padding declared earlier, and the photo wall's
   `padding: 0` on the toolbar must lose as well -- a toolbar of no height still has to hold the
   wall's heading clear of the status bar, and an inset's worth of padding is exactly that.
   --------------------------------------------------------------------------------------------- */

/* THE GLASS FADES OUT LOWER THAN THE INSET (Jon's screenshot, 2026-09-20, 21:21). With the padding
   above in place the toolbar sat below the clock and the fog still took the top third of the logo:
   the inset is the height of the status bar, and Liquid Glass feathers its blur roughly 24pt past
   that (measured off the screenshot at 3x -- the toolbar's rule at ~110pt, the fog ending at ~83pt,
   the inset 59pt). So the top clearance is the inset PLUS the feather, and it is one number with a
   name, because it is a measurement of somebody else's chrome and will want tuning by eye.

   Only in the installed app, which is the only place the fog was ever seen; in a browser tab the
   clearance is the bare inset. And only where there IS an inset: `min( feather, inset * 100 )` is
   0 when the inset is 0 and the whole feather as soon as it is not, so an installed app on a
   desktop, which has no status bar, gains no band of nothing. */
body.app-shell-body
{
	/* 0 since the sticky shell below took the blur away; 1.5rem is the figure measured off the
	   screenshot, and what to put back if the fog ever returns over the head of the toolbar. */
	--glass-feather: 0rem;
	--top-clearance: env( safe-area-inset-top, 0px );
}

@media ( display-mode: standalone )
{
	body.app-shell-body
	{
		--top-clearance: calc( env( safe-area-inset-top, 0px ) + min( var( --glass-feather ), env( safe-area-inset-top, 0px ) * 100 ) );
	}
}

.app-shell
{
	/* THE LINE THAT ACTUALLY STOPS THE FOG (Jon, 2026-09-20, found by trying it in the inspector on
	   the phone). Liquid Glass decides what to draw under the status bar by looking for something
	   PINNED to the top edge: with a sticky or fixed element there it takes that element's ground as
	   a flat tint, and with none it blurs whatever page is beneath -- which was the logo. The shell
	   never scrolls (body.app-shell-body is `overflow: hidden`), so `sticky` moves nothing; it is
	   here purely to be seen by the platform. Do not tidy it away as a no-op.

	   It does make the shell a stacking context and a containing block for absolutely positioned
	   descendants. Checked when it landed: the dock, the diagnostics panel, the toasts and the
	   floaters are all INSIDE the shell and keep their order; what scripts append to <body> is a
	   top-layer <dialog> or carries a z-index above it; and sticky does not capture `position:
	   fixed`, so the sheet and the floaters still measure from the viewport. */
	position: sticky;
	top: 0;

	/* A phone on its side: the notch is on one of these. */
	padding-left: env( safe-area-inset-left, 0px );
	padding-right: env( safe-area-inset-right, 0px );
}

body.app-shell-body .app-toolbar
{
	padding-top: var( --top-clearance );
}

/* The toolbar is border-box, so its floor has to grow by what the padding took or the controls are
   squeezed by it. Not on the photo wall, whose toolbar has no floor to keep. */
body.app-shell-body:not( [data-library-layout="grid"] ) .app-toolbar
{
	min-height: calc( var( --toolbar-height ) + var( --top-clearance ) );
}

[data-library-layout="grid"] .grid-controls
{
	top: calc( .75rem + var( --top-clearance ) );
}

/* The wide sidebar is a column that reaches the top of the screen (an iPad's home-screen app); the
   narrow one is a sheet from the bottom and has no top to protect. */
@media not all and ( max-width: 46rem )
{
	.app-sidebar
	{
		padding-top: var( --top-clearance );
	}
}

/* The home indicator. The floaters, the sheet and the item menu already reserve this inset for
   themselves (D352, D427); until the viewport covered the screen it was always 0 for them too. */
.app-footer
{
	min-height: calc( var( --footer-height ) + env( safe-area-inset-bottom, 0px ) );
	padding-bottom: env( safe-area-inset-bottom, 0px );
}


/* ---------------------------------------------------------------------------------------------
   Putting the sidebar away (D408).

   `<body data-sidebar-hidden="true">` takes the sidebar's column out of the grid outright -- the
   photo wall's rule above, for the photo wall's reason: a zero-width column still paints its border
   and still takes Tab. The two footer buttons that toggle it share the footer's left edge, and
   exactly one of them is drawn at a time.

   ALL OF IT IS SCOPED TO A WIDE SCREEN, with the exact complement of the sheet's query. An account
   that keeps the sidebar away still gets the sheet and its floater on a phone: there the sidebar is
   already away until it is asked for, and hiding the sheet as well would leave a phone with no
   navigation at all.

   The flight -- the wordmark's B flying down into the footer mark and back -- is sidebar.js's, done
   with the Web Animations API on a fixed clone. The rules for the clone are here so that the script
   sets geometry and nothing else.
   --------------------------------------------------------------------------------------------- */

.sidebar-hide,
.sidebar-restore
{
	display: none;
}

.sidebar-restore-mark
{
	width: 1.45rem;
	height: 1.45rem;
	flex: none;
}

.sidebar-restore-mark-light { display: var( --logo-light-display ); }
.sidebar-restore-mark-dark { display: var( --logo-dark-display ); }

@media not all and ( max-width: 46rem )
{
	/* The footer's tray buttons' shape (Jon, 2026-09-11: a thing you press is drawn as a button), at
	   the same height, so the row reads as one set of controls with a gap in the middle. */
	.app-footer .sidebar-hide,
	.app-footer .sidebar-restore
	{
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: .35rem;
		flex: none;
		min-height: calc( var( --footer-height ) - .65rem );
		padding: 0 .65rem;
		border: 1px solid var( --rule );
		border-radius: 999px;
		background: var( --card-background );
		color: var( --ink-quiet );
		font: inherit;
		cursor: pointer;
		box-shadow: 0 1px 1px rgba( 0, 0, 0, .05 );
	}

	/* The mark carries its own margin inside its square, so the pill's left padding is shaved to
	   make the gap either side of the picture look even -- optical, as the wordmark's is. */
	.app-footer .sidebar-restore
	{
		padding-left: .45rem;
	}

	.app-footer .sidebar-hide .icon
	{
		--icon-size: 1.1rem;
	}

	.app-footer .sidebar-hide:hover,
	.app-footer .sidebar-restore:hover
	{
		border-color: var( --ink-quiet );
		background: color-mix( in srgb, var( --ink ) 4%, var( --card-background ) );
		color: var( --ink );
	}

	.app-footer .sidebar-hide:focus-visible,
	.app-footer .sidebar-restore:focus-visible
	{
		outline: 2px solid var( --accent );
		outline-offset: 2px;
	}

	body[data-sidebar-hidden="true"] .app-footer .sidebar-hide,
	body:not( [data-sidebar-hidden="true"] ) .app-footer .sidebar-restore
	{
		display: none;
	}

	body[data-sidebar-hidden="true"] .app-shell
	{
		grid-template-columns: minmax( 0, 1fr );
		grid-template-areas:
			"content"
			"footer";
	}

	body[data-sidebar-hidden="true"] .app-sidebar
	{
		display: none;
	}
}

/* Where a flight is going, while it is going there: the clone draws the B, and the real one would be
   a second B under it that arrives before the flight does. */
.sidebar-restore[data-mark-landing] .sidebar-restore-mark
{
	visibility: hidden;
}

/* The clone. Fixed, so it can cross from the sidebar into the footer without belonging to either;
   origin at its top-left corner, so a scale and a translate measured from two bounding boxes compose
   without a correction term; above the toasts and the trays, and never a target. */
.sidebar-mark-flight
{
	position: fixed;
	z-index: 50;
	margin: 0;
	transform-origin: 0 0;
	pointer-events: none;
	will-change: transform;
}


/* ---------------------------------------------------------------------------------------------
   Pin affordances. SERVER-RENDERED since D222, which reverses what this block used to say.

   They were injected by app.js, because favorites lived in localStorage and the server neither had
   the state nor should have had it. Schema v13 gives it the state -- see app/lib/favorites.php for
   the access filter that arrives with it -- so every pin is now a real <form> in the document. The
   visual grammar below is unchanged: a star that appears on hover, and stays lit when it is on.

   Two styles. `.pin-button` is the compact star on a row, a tile or a card. `.pin-pill` is the
   worded control that sits in an action bar beside "Rename…" on a container page and beside the
   title on an item page -- the discoverable route, which is the half that did not exist.
   --------------------------------------------------------------------------------------------- */

/* The form is a wrapper with no box of its own: it must not disturb the layout of the row, tile or
   card it sits in. Everything visual belongs to the button inside it. */
.pin-form
{
	display: contents;
}

.pin-button
{
	border: 0;
	background: none;
	color: var( --ink-quiet );
	font-size: var( --text-md );
	line-height: 1;
	padding: 0 .15rem;
	cursor: pointer;
	opacity: 0;
	transition: opacity .12s ease, color .12s ease;
}

/* Hover on the ROW, not on the button: a control you must already be touching to see is a control
   nobody finds. This is the discoverability half of D222 -- the gesture that existed was a drag with
   no affordance at all, and the answer is not a second invisible thing. */
.sidebar-row:hover ~ .pin-form .pin-button,
.row-with-control:hover .pin-button,
.favorite-item:hover .pin-button,
.scope-tile-item:hover .pin-button,
.favorite-tile-item:hover .pin-button,
.pin-button:focus-visible
{
	opacity: 1;
}

/* A favorited thing keeps its star lit, always. The state has to be readable without hovering every
   row in the tree to find out which ones are favorites.

   GOLD, not accent (Jon, 2026-09-04). The lit star used to be `--accent`, which is the colour of
   every submit button on the page -- so a favorite and a "Save" were drawn in one hue while being
   two entirely different kinds of fact: one is a page's next step, the other is a private note a
   person made to themselves that nobody else can even see (D268). Gold separates them, and it is
   the colour a star already is everywhere a person has ever met one. The hover is gold too, so the
   colour ANNOUNCES the act before it is committed rather than arriving as a surprise afterwards. */
.pin-button[data-pinned="true"]
{
	opacity: 1;
	color: var( --favorite );
}

.pin-button:hover
{
	color: var( --favorite );
}

/* On a tile, the star sits in the corner the way a card's does rather than in the text flow. */
.scope-tile-item,
.favorite-tile-item
{
	position: relative;
}

.scope-tile-item .pin-button,
.favorite-tile-item .pin-button
{
	position: absolute;
	top: .45rem;
	right: .5rem;
	z-index: 2;
}

/* The worded control. Always visible: it is the route somebody is looking FOR, so it may not hide. */
.pin-pill
{
	display: inline-flex;
	align-items: center;
	gap: .35rem;
	cursor: pointer;
}

/* The worded control, lit. Gold in all three of its parts -- the star, the word and the border --
   because a pill whose star is gold and whose text is not reads as two controls in one box. It is
   NOT a `data-tone`: the four tones say what pressing a control DOES, and this says what is already
   true of the thing it names, which is why it is a state on one control rather than a fifth entry
   in a vocabulary about verbs. */
.pin-pill[data-pinned="true"]
{
	color: var( --favorite );
	border-color: var( --favorite );
	background: color-mix( in srgb, var( --favorite ) 8%, var( --card-background ) );
	font-weight: 600;
}

.pin-pill[data-pinned="true"]:hover
{
	border-color: var( --favorite );
	background: var( --favorite-quiet );
}

.pin-pill .pin-star
{
	font-size: var( --text-md );
	line-height: 1;
}

/* A sidebar row that carries a control lays out like a favorite row: anchor takes the space, the
   control sits at the end. One rule, so the two cannot drift apart. */
.row-with-control
{
	display: flex;
	align-items: center;
}

.row-with-control > .sidebar-row
{
	flex: 1;
	min-width: 0;
}

/* A collapsible row's children are a <ul> inside the same <li>, and a flex row would lay them out
   beside the row instead of under it. The wrap puts them back on their own line. */
.row-with-control
{
	flex-wrap: wrap;
}

.row-with-control > .sidebar-list
{
	flex-basis: 100%;

	/* A flex item's floor is otherwise its min-content width, which for a list of `nowrap` rows is the
	   longest label at full length plus its indent -- so a deep folder with a long name widened every
	   list above it past the sidebar, the label's ellipsis never engaged, and the scroll region
	   scrolled sideways when the filter opened the branch and scrolled to the match (Jon, 2026-10-02).
	   The row beside it already says the same thing. */
	min-width: 0;
}

.item-pin-actions
{
	margin: 0 0 .75rem;
}

/* THE ITEM TOOLBAR: a row of pills beside the title (itemview.php's renderItemMainRegion).

   A flex row rather than inline elements in a block, for the gap alone -- inline-flex pills separated
   by whitespace collapse to a single space at the browser's discretion, and two controls .3rem apart
   read as one control with a seam in it. `wrap` because the download pill carries a size and a very
   large one ("Download original 3.3 GB") is wide enough to push the pair past a phone's width; the
   alternative is a horizontal scroll on the page body, which app.css refuses everywhere else.

   `align-items: center` matters because the two children are not the same height by nature: the pin
   is a <button> whose .pin-form wrapper is `display: contents`, so it lands here as a direct flex
   child, while the download is an <a> carrying a second line of text in a .quiet span. */
.item-toolbar
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .4rem;
}

/* Replace source (D325). The label is a pill like its neighbours; the progress card that appears
   once a file is chosen takes a whole row under the pills, which is why the wrapper contributes its
   children to the flex row rather than a box of its own. */
.item-toolbar .item-replace-source { display: contents }
.item-toolbar .replace-cards { flex-basis: 100%; width: 100% }
.item-toolbar .replace-cards[hidden] { display: none }

/* The Versions table's "Make current" form sits in a cell; a block form there adds a margin nothing
   asked for. */
.item-versions form { margin: 0 }

/* The download rows (D543): the item's representation list -- the original first, then each rung --
   as a wrapping row of pills, drawn in the Web video and Web renditions areas and, for a family with
   neither, in the header where the single pill used to be. A list rather than a paragraph because
   each row is one file a person may take, which is what a screen reader should count. */
.item-downloads
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .4rem;
	margin: .5rem 0 0;
	padding: 0;
	list-style: none;
}

.item-toolbar .item-downloads { display: contents }

/* The arrow, sized to match .pin-pill's star exactly. The pills sit side by side, so a glyph a
   pixel off in either dimension is visible in a way it would not be alone. */
.item-download .pill-glyph
{
	font-size: var( --text-md );
	line-height: 1;
}

/* The format and the size, said quietly. They qualify the label rather than being part of it --
   "Original" is the act, "MOV · 701 MB" is what it will cost -- and .quiet already carries that
   meaning everywhere else on the page. */
.item-download .quiet
{
	font-size: var( --text-xs );
}

/* The Favorites section as a drop target. The whole section lights up, not the list inside it: an
   empty list is zero pixels tall, so the first pin of a person's life had nothing to aim at.

   It wears the same "into" as every other container -- the shared rule in the MOVING THINGS block,
   solid and filled. It used to be dashed, which is what the grid draws for REFUSED (D300). Only the
   geometry is its own: a section has no padding for an inset outline to sit in, so the ring goes
   outside. */
.sidebar-section[data-section="favorites"][data-drop-state="into"]
{
	outline-offset: 2px;
	border-radius: .4rem;
}


/* ON A CARD THE STAR AND THE KEBAB LIVE IN THE BODY'S TOP RIGHT CORNER, ALWAYS VISIBLE (Jon,
   2026-09-18: "buttons on top of the image are hard to see"). They used to sit over the preview on
   a translucent disc and appear on hover; over a bright or busy picture the disc was not enough,
   and a control that must be hovered to be found is not there for a finger. In the body they are on
   the card's own background, so they need no disc and no hover.

   `grid-row: 2` is what puts them there: the card is a two-row grid, and an absolutely positioned
   child that names a grid area takes that area as its containing block without occupying a cell
   (the list layout's kebab uses the same rule for the title column). The kebab has the right edge
   and the star is second, so the pair reads outward from the item: mine, then everything else. */
.item-card .pin-button
{
	position: absolute;
	grid-row: 2;
	/* Level with the title's first line, which sits under the body's .75rem of padding. */
	top: .5rem;
	right: calc( .3rem + var( --icon-button-size ) );
	z-index: 2;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	display: grid;
	place-items: center;
	font-size: var( --icon-button-glyph );
	padding: 0;
	opacity: 1;
}

.favorite-item
{
	display: flex;
	align-items: center;
}

/* Jon, 2026-09-03, folded in from experiments.css. Every other row in the tree carries a disclosure
   triangle in front of its icon; a favourite has nothing to disclose, so without this its icon sits
   a triangle's width to the left of every icon below it and the column reads as broken. */
section[data-section="favorites"] .favorite-item
{
	padding-left: 1.2rem;
}

.favorite-item .sidebar-row
{
	flex: 1;
	min-width: 0;
}

/* ---------------------------------------------------------------------------------------------
   A favorite whose target is gone.

   Pinned things outlive what they point at -- an item is erased, a folder is renamed, somebody else
   tidies. The row is greyed and given a dash rather than deleted: a person put it there deliberately,
   and an interface that quietly removes their work to keep its own list tidy has made a decision
   that was not its to make. The × beside it is how it goes, when they say so.

   It also un-greys itself. A removed item can be restored (D136), and a favorite left greyed after
   its target came back would be wrong in the other direction.
   --------------------------------------------------------------------------------------------- */

.favorite-row[data-stale="true"]
{
	opacity: .55;
}

.favorite-row[data-stale="true"] .row-label
{
	text-decoration: line-through;
	text-decoration-thickness: 1px;
	text-decoration-color: var( --ink-quiet );
}

.favorite-stale
{
	margin-left: auto;
	padding-left: .35rem;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
	line-height: 1;
}

.row-unpin
{
	border: 0;
	background: none;
	color: var( --ink-quiet );
	font-size: var( --text-base );
	line-height: 1;
	padding: 0 .1rem;
	cursor: pointer;
	opacity: 0;
}

.favorite-item:hover .row-unpin,
.row-unpin:focus-visible
{
	opacity: 1;
}

/* ---------------------------------------------------------------------------------------------
   THE DOCK: the queue tray and the upload tray, bottom right.

   Fixed to the viewport rather than placed in the grid, because it must not shift the layout: an
   upload starting should never move the thing someone was about to click (ui-design.md §1.2). It
   sits above the footer and out of the scroll flow, so it is equally present on every view -- which
   is the point of hosting it in the shell chrome at all.

   Both trays are permanent in the MARKUP and absent from the screen until summoned. That is the
   distinction that matters, and it is what the old appear-when-busy tray could not do: the surfaces
   that answer "what is the machine doing?" are consultable when it appears to be doing nothing,
   which is when people ask -- but they are not parked over the content the rest of the time. The
   footer's two buttons are the permanent part, and they cost a row that already existed.
   --------------------------------------------------------------------------------------------- */

.shell-dock
{
	position: fixed;
	right: 1rem;
	bottom: calc( var( --footer-height ) + 1rem + env( safe-area-inset-bottom, 0px ) );
	z-index: 30;
	/* Two trays SIDE BY SIDE, not stacked. Stacked, opening the lower one pushed the upper one up
	   and opening both put one on top of the other in the only corner either can occupy -- so
	   reading the queue while an upload ran meant closing the thing you were watching. Side by side
	   they are two independent surfaces and neither move is a trade against the other. Width is
	   twice the old single-tray figure plus the gap, so each tray keeps the 24rem it was designed
	   at rather than each getting half of it.

	   **Three abreast since the Shelf (D405, 2026-09-16)**: three 24rem trays and two gaps, 73.2rem,
	   which was the first of the two answers this comment used to offer. Jon took it -- *"we're not
	   expecting more of them"* -- with the other half being that they SHRINK when all three are open
	   on a viewport narrower than that, which `flex: 0 1 24rem` on .dock-tray already does. The
	   51rem breakpoint below is the floor for two, and it still is: three narrowed trays between
	   51rem and 73rem beat three stacked.

	   **Two again since D589**: the Shelf left the dock to become a drawer at the right edge, so the
	   dock holds the uploads and the queue -- two 24rem trays and one gap. */
	width: min( 48.6rem, calc( 100vw - 2rem ) );
	display: flex;
	flex-direction: row;
	/* Bottom-anchored, and this is now doing two jobs. `align-items` is what makes an opening tray
	   grow UPWARDS from a fixed bottom edge, so the header you just clicked stays under the cursor
	   and its neighbour does not move at all. `justify-content` keeps the pair pinned to the right
	   when the viewport is wider than they are. */
	align-items: flex-end;
	justify-content: flex-end;
	gap: .6rem;
	max-height: calc( 100vh - var( --footer-height ) - 3rem );
	pointer-events: none;
}

/* The gaps between the trays are not the dock's business to intercept: clicks there belong to
   whatever is underneath. Each tray takes its own pointer events back. */
.dock-tray
{
	pointer-events: auto;
	display: flex;
	flex-direction: column;
	/* A tray is 24rem wide, and it MAY SHRINK BUT NEVER GROW. The grow half is the one that bit:
	   with `flex-grow: 1` a tray opened on its own stretched across the dock's full two-tray width,
	   so its left edge floated 24rem out from the corner it is supposed to sit in -- and on a
	   middling viewport that put it at the left edge of the screen. `flex-grow: 0` makes one tray
	   the same width as either of two, and `justify-content: flex-end` on the dock keeps it in the
	   corner; the shrink half is what lets a pair narrow together rather than overflow.

	   `min-width: 0` because a flex item's default `min-width: auto` refuses to shrink below its
	   content -- which here is a job title of arbitrary length, so without it one tray silently
	   pushes the other out of the dock. */
	flex: 0 1 24rem;
	min-width: 0;
	min-height: 0;
	background: var( --card-background );
	border: 1px solid var( --rule );
	border-radius: .6rem;
	box-shadow: 0 .6rem 2rem rgba( 0, 0, 0, .22 );
	overflow: hidden;
}

.dock-tray-head
{
	/* Never shrinks. When both trays are open and the dock hits its max-height, the flex shrinking
	   has to come out of the bodies -- which scroll -- and not out of the title bars, which are the
	   part that has to stay legible for the tray to be identifiable at all. */
	flex: none;
	display: flex;
	align-items: stretch;
	background: color-mix( in srgb, var( --card-background ) 88%, var( --page-background ) );
	border-bottom: 1px solid var( --rule );
}

/* The whole header is the button, title and × together -- the same rule the sidebar's section
   headings follow, and for the same reason: a 12px glyph beside an inert word asks a person to hit
   the smallest thing on the row. */
.dock-tray-toggle
{
	flex: 1;
	display: flex;
	align-items: center;
	gap: .45rem;
	border: 0;
	background: none;
	font: inherit;
	color: var( --ink-quiet );
	cursor: pointer;
	padding: .35rem .4rem .35rem .8rem;
	text-align: left;
}

.dock-tray-toggle:hover { color: var( --ink ); }
.dock-tray-toggle:focus-visible { outline: 2px solid var( --accent ); outline-offset: -2px; }

.dock-tray-title { font-size: var( --text-sm ); font-weight: 600; }

/* ---------------------------------------------------------------------------------------------
   THE SHELF (D405): a tray of pointers, and the pointer look.

   An entry is a row -- the picture or glyph, the name, a line of meta, and the × that takes it off
   -- and it wears the POINTER vocabulary: `.pointer` on the row, `.pointer-badge` on the corner of
   the glyph, a small accent disc carrying the link glyph, which is the file systems' curled arrow in
   this icon set. The item alias (design §3.4) will wear exactly this, so it is deliberately not
   named after the shelf. The row is a drag source, so it lifts on hover the way the title handle
   does (grab cursor, quiet fill) rather than reading as a plain list.
   --------------------------------------------------------------------------------------------- */
.shelf-list
{
	list-style: none;
	margin: 0;
	padding: .3rem;
	display: flex;
	flex-direction: column;
	gap: .15rem;
}

.shelf-entry
{
	display: flex;
	align-items: center;
	gap: .3rem;
	border-radius: .45rem;
	-webkit-user-select: none;
	user-select: none;
	-webkit-touch-callout: none;
}

.shelf-entry[draggable="true"] { cursor: grab; }

.shelf-entry[draggable="true"]:hover
{
	background: var( --accent-quiet );
	box-shadow: inset 0 0 0 1px color-mix( in srgb, var( --accent ) 40%, transparent );
}

.shelf-entry[data-stale="true"] { opacity: .6; }

.shelf-entry-link
{
	flex: 1;
	min-width: 0;
	display: flex;
	align-items: center;
	gap: .6rem;
	padding: .35rem .4rem;
	color: var( --ink );
	text-decoration: none;
}

.shelf-entry-glyph
{
	flex: none;
	position: relative;
	width: 2.4rem;
	height: 2.4rem;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: .4rem;
	background: color-mix( in srgb, var( --ink ) 6%, var( --card-background ) );
	color: var( --ink-quiet );
	overflow: visible;
}

.shelf-entry-thumb
{
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: .4rem;
}

/* The badge: a disc over the glyph's bottom-left corner, as an alias badge sits on a file icon.
   Accent on card-background so it reads on a photograph and on a glyph alike. */
.pointer-badge
{
	position: absolute;
	left: -.3rem;
	bottom: -.3rem;
	width: 1.05rem;
	height: 1.05rem;
	--icon-size: .62rem;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 50%;
	background: var( --accent );
	color: var( --card-background );
	box-shadow: 0 0 0 2px var( --card-background );
}

/* THE ALIAS CARD (D584): an item drawn in a folder by reference. It wears the pointer look above --
   the badge on the picture's corner -- and a dashed accent frame, which is the "somehow ghostly
   representation" Jon described: the same card, plainly not the thing itself. One block for every
   layout, as the card's markup is one for every layout (D208). */
.item-card.pointer
{
	outline: 1.5px dashed color-mix( in srgb, var( --accent ) 60%, transparent );
	outline-offset: -1.5px;
}

.item-card.pointer .card-preview { position: relative; }

.card-alias-badge
{
	left: .45rem;
	bottom: .45rem;
	width: 1.45rem;
	height: 1.45rem;
	--icon-size: .8rem;
	z-index: 2;
}

.card-alias-line
{
	margin: 0;
	display: flex;
	align-items: center;
	gap: .3rem;
	font-size: var( --text-xs );
	color: var( --ink-quiet );
	--icon-size: .75rem;
}

.card-alias-line a { color: inherit; text-decoration: none; }
.card-alias-line a:hover { color: var( --ink ); text-decoration: underline; }

.shelf-entry-text
{
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: .05rem;
}

.shelf-entry-name
{
	font-size: var( --text-sm );
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.shelf-entry-meta
{
	font-size: var( --text-xs );
	color: var( --ink-quiet );
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The take-off ×: quiet until the row is hovered, as the sidebar's row tools are (a control you
   must already be pointing at to see is a control nobody finds -- but here the row is the thing
   pointed at, so the × appears with it). */
.shelf-entry .shelf-form { flex: none; }

.shelf-take
{
	--icon-size: var( --icon-button-glyph );
	display: flex;
	align-items: center;
	justify-content: center;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	margin-right: .3rem;
	border: 0;
	border-radius: .35rem;
	background: none;
	color: var( --ink-quiet );
	cursor: pointer;
	opacity: 0;
}

.shelf-entry:hover .shelf-take,
.shelf-take:focus-visible { opacity: 1; }
.shelf-take:hover { background: color-mix( in srgb, var( --ink ) 8%, transparent ); color: var( --ink ); }

/* A finger has no hover, so on a touch screen the rule above made this button not small but ABSENT:
   an entry could be put on the shelf from a phone and never taken off it (seen 2026-09-18, D430). */
@media ( hover: none )
{
	.shelf-take { opacity: 1; }
}

.shelf-clear-form
{
	display: flex;
	justify-content: flex-end;
	padding: .3rem .5rem .5rem;
}

.shelf-hidden-note { padding: 0 .6rem; }

/* The pill on an item's page: the pin pill's shape, with the badge glyph instead of a star. */
.shelf-pill { gap: .35rem; }
.shelf-pill .shelf-pill-glyph { --icon-size: .8em; display: inline-flex; }
.shelf-pill[data-shelved="true"]
{
	background: var( --accent-quiet );
	border-color: color-mix( in srgb, var( --accent ) 45%, var( --rule ) );
}

/* The footer's Shelf button became the Shelf's handle at D590; its styles are .shelf-handle's. */

/* A tray body armed as a target: the generic ring, on the tray rather than the body, because the
   body may be one short paragraph. */
.dock-tray[data-drop-state="into"] { outline-offset: -3px; }

/* THE FAUX CLOSE BUTTON (Jon, 2026-09-11). Drawn as a small square control at the header's right
   edge, but it is not one: it is aria-hidden and has no handler, and the header button it sits in is
   what a click lands on. That is the point -- the header always collapsed the tray and nothing said
   so; the × says so without adding a second control that could come to disagree with the first
   (D165). It lights up on hovering ANY part of the header, because any part of it does the job. */
.dock-tray-close
{
	margin-left: auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	border-radius: .35rem;
	color: var( --ink-quiet );
	--icon-size: var( --icon-button-glyph );
}

.dock-tray-toggle:hover .dock-tray-close
{
	background: color-mix( in srgb, var( --ink ) 10%, transparent );
	color: var( --ink );
}

/* THE "OPEN AS A PAGE" LINK (D386). A real <a>, parked over the header's right end just left of the
   faux ×, so the header stays one button with one link on top of it rather than becoming three
   controls: the header is `position: relative` for this alone. Same size and hover as the ×, so
   the two read as a pair of window controls -- open out, close -- which is what they are. */
.dock-tray-head { position: relative; }

.dock-tray-expand
{
	position: absolute;
	right: calc( .6rem + var( --icon-button-size ) );
	top: 50%;
	transform: translateY( -50% );
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	border-radius: .35rem;
	color: var( --ink-quiet );
	--icon-size: var( --icon-button-glyph );
}

.dock-tray-expand:hover
{
	background: color-mix( in srgb, var( --ink ) 10%, transparent );
	color: var( --ink );
}

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

.dock-tray-body
{
	overflow-y: auto;
	min-height: 0;
	max-height: min( 40vh, 22rem );
}

/* **Closed means GONE, not folded.** A collapsed tray used to leave its title bar on screen, so the
   resting dock was a permanent strip in the corner of every view whether or not anybody had asked
   for it -- and with the footer's two buttons now saying everything a title bar said (a reading, a
   dot, an attention badge), that strip was a second copy of the answer parked on top of the content.
   The buttons summon these and dismiss them; between times the dock occupies nothing.

   `display: none` and not `visibility: hidden`: the dock is a flex row, and a merely invisible tray
   would still take its half of the row and push the other one out of the corner. It also has to be
   the TRAY and not its parts -- hiding only the body and the foot is what left the title bar behind. */
.dock-tray[data-collapsed="true"] { display: none; }


/* ---------------------------------------------------------------------------------------------
   THE SHELF IS A FLOATING DRAWER, OPENED BY A HANDLE (D589, D590). Jon, after trying D589's grid
   column: *"the amount of layout change the shelf causes get problematic. Let's try a floating version
   and have the button as a simple right side aligned vertical handle"*, and *"limit its height to not
   cover the top toolbar"*. So it takes NO width from the page -- opening it moves no card -- and runs
   from the toolbar's bottom edge to the footer's top, over the page at the right edge.

   The top is the toolbar's FLOOR before any script runs, and its real bottom edge once app.js has
   measured it (`--shelf-drawer-top`, kept current by a ResizeObserver): the toolbar wraps on a
   narrow window, and a drawer cut to its floor would cover the second row.

   CLOSED is `display: none`, as a collapsed tray always was. The handle is always there: a slim
   vertical tab on the right edge, riding on the drawer's left edge while it is open (it follows the
   drawer in the document, so `~` finds it). It is what the footer's Shelf button was -- the toggle,
   the count, a drop target, the thing a held drag springs open.
   --------------------------------------------------------------------------------------------- */
.app-shell
{
	--shelf-drawer-width: min( 20rem, 85vw );
	--shelf-drawer-top-floor: calc( var( --toolbar-height ) + var( --top-clearance, 0px ) );
}

.dock-tray.shelf-drawer
{
	position: fixed;
	top: var( --shelf-drawer-top, var( --shelf-drawer-top-floor ) );
	right: 0;
	bottom: calc( var( --footer-height ) + env( safe-area-inset-bottom, 0px ) );
	width: var( --shelf-drawer-width );
	z-index: 40;
	min-height: 0;
	border: 0;
	border-left: 1px solid var( --rule );
	border-top: 1px solid var( --rule );
	border-radius: .6rem 0 0 0;
	box-shadow: -.6rem 0 2rem rgba( 0, 0, 0, .18 );
	background: var( --card-background );
}

.shelf-drawer .dock-tray-body
{
	flex: 1 1 auto;
	max-height: none;
}

.shelf-handle
{
	position: fixed;
	right: 0;
	top: 50%;
	transform: translateY( -50% );
	z-index: 41;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: .4rem;
	padding: .7rem .3rem;
	min-height: 6.5rem;
	border: 1px dashed var( --rule );
	border-right: 0;
	border-radius: .55rem 0 0 .55rem;
	background: var( --card-background );
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-xs );
	cursor: pointer;
	box-shadow: -.2rem 0 .8rem rgba( 0, 0, 0, .08 );
}

/* The count reads top to bottom, as a spine does. */
.shelf-handle [data-status-text="shelf"]
{
	writing-mode: vertical-rl;
	white-space: nowrap;
}

.shelf-handle .shelf-button-glyph { --icon-size: .8rem; display: inline-flex; }

.shelf-handle:hover { color: var( --ink ); }
.shelf-handle:focus-visible { outline: 2px solid var( --accent ); outline-offset: -2px; }

.shelf-handle[aria-expanded="true"],
.shelf-handle[data-drop-state]
{
	border-style: solid;
	color: var( --ink );
}

.shelf-drawer[data-collapsed="false"] ~ .shelf-handle { right: var( --shelf-drawer-width ); }

@media print { .shelf-handle { display: none; } }

/* The queue tray's own footer: the worker's state and the install urn, both of which used to sit in
   the shell footer's left half. Quiet, small, and last -- they are the standing conditions the rows
   above happen under, not news. */
.dock-tray-foot
{
	flex: none;
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: .35rem;
	margin: 0;
	padding: .4rem .7rem;
	border-top: 1px solid var( --rule );
	background: color-mix( in srgb, var( --card-background ) 88%, var( --page-background ) );
	font-size: var( --text-xs );
	color: var( --ink-quiet );
}

/* The tone attribute setFooterText() writes still has to mean something now that this element is
   outside .app-footer, where the only rules for it lived. A paused worker is an operator decision
   rather than a fault (D65), so it is warning-coloured here and raised as attention on the footer
   button -- said once in each place, never twice in one. */
.dock-tray-foot .status-group[data-tone="warning"] { color: var( --warning ); }

.dock-tray-foot-separator { opacity: .5; }

/* The urn is the least urgent thing in the dock and is allowed to be the first thing to break onto
   a second line; it must never widen the tray to fit itself. */
.dock-tray-foot-urn
{
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dock-tray-empty,
.dock-tray-caption
{
	margin: 0;
	padding: .5rem .7rem;
	font-size: var( --text-xs );
	color: var( --ink-quiet );
}

.dock-tray-caption
{
	font-style: normal;
	text-transform: uppercase;
	letter-spacing: .04em;
	padding-bottom: .2rem;
}

/* A sentence, not a label, and an italic one in the quiet ink -- so it takes the step above the
   caption it shares a rule with. At the caption's size it was the smallest prose in the shell. */
.dock-tray-empty { font-style: italic; font-size: var( --text-sm ); }

.dock-rows
{
	list-style: none;
	margin: 0;
	padding: 0 .35rem .35rem;
}

.dock-row
{
	display: flex;
	align-items: center;
	gap: .5rem;
	padding: .3rem .35rem;
	border-radius: .35rem;
	text-decoration: none;
	color: inherit;
}

.dock-row:hover { background: var( --accent-quiet ); }

.dock-row-glyph
{
	flex: none;
	width: 1rem;
	text-align: center;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
}

.dock-row[data-job-status="running"] .dock-row-glyph { color: var( --accent ); }
.dock-row[data-job-status="failed"] .dock-row-glyph,
.dock-row[data-job-status="dead"] .dock-row-glyph { color: var( --warning ); }

/* min-width:0 on a flex child is what lets the ellipsis below actually happen; without it the child
   refuses to shrink below its content and the row overflows the tray instead. */
.dock-row-text { min-width: 0; display: flex; flex-direction: column; }

.dock-row-name,
.dock-row-meta
{
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dock-row-name { font-size: var( --text-sm ); }
.dock-row-meta { font-size: var( --text-xs ); color: var( --ink-quiet ); }

/* A row with an ok (D393) is the link and the button side by side: the link keeps the whole width
   it had, the button sits at the end and never wraps under it. Sized like the FYI card's dismiss,
   which is the other small "I have seen this" in the shell. */
.dock-rows > li { display: flex; align-items: center; gap: .35rem; }
.dock-rows > li > .dock-row { flex: 1 1 auto; min-width: 0; }
.dock-row-ok { flex: none; font-size: var( --text-xs ); padding: .2rem .5rem; }

/* The upload cards' own host, inside the upload tray's body. `margin-top: 0` undoes upload.css's
   2rem, which spaces the list from the drop zone on the standalone upload page and was an empty band
   above the first card in here. */
.dock-tray-body #uploads
{
	margin-top: 0;
	padding: .6rem;
	display: flex;
	flex-direction: column;
	gap: .6rem;
}

/* Empty and it collapses to nothing, so the history sits directly under the header rather than
   below a band of padding belonging to a list with no members. */
.dock-tray-body #uploads:empty { display: none; }

.dock-tray-history[hidden] { display: none; }

/* Side by side is the design (both trays consultable at once) but it has a hard floor: two 24rem
   trays plus the gap and the margins want ~50.6rem, and below that "side by side" would mean two
   columns too narrow to read a filename in. Under the floor they stack again, which is the older
   behaviour and the correct one at that size -- one tray at readable width beats two at neither. */
@media ( max-width: 51rem )
{
	.shell-dock
	{
		width: min( 24rem, calc( 100vw - 2rem ) );
		flex-direction: column;
		align-items: stretch;
		gap: .4rem;
	}

	/* Basis back to auto: stacked, `flex-basis: 24rem` is a HEIGHT, and would floor every tray at
	   24rem tall whether or not it had that much in it. */
	.dock-tray { flex: 0 1 auto; }
}

/* The footer's tray buttons. A <span> that stated a number became a button that opens the list it
   counts -- and until 2026-09-11 it still LOOKED like the span, bare footer text with a hover colour,
   so nothing said it could be pressed until it was. Jon asked for them bigger and more like buttons,
   and they now wear the pressable shape (round and bordered: a thing you press is round, D129).

   Their height is the footer band's, not --control-height, and that is a declared exception of the
   same kind as `.toolbar-controls`: chrome sized to the strip it lives in. At the system height the
   footer would have to grow to ~3rem to give them a margin, which is a larger bite of every screen
   than a status row is worth.

   PRESSED WHILE ITS TRAY IS OPEN. aria-expanded is written by applyDockTrayState() for both of a
   tray's controls, so drawing the state off the attribute means the look cannot disagree with what
   a screen reader is told. It is the only indication in the footer of which tray is up, and with a
   third tray coming (the Shelf) that stops being obvious from the corner of the screen alone.

   The tone rules above (`data-tone`) still colour the text; the border takes a tint of the same
   tone so a warning-toned button reads as one at the edge, where the eye finds a button first. */
.app-footer .status-tray-toggle
{
	gap: .45rem;
	min-width: 0;
	min-height: calc( var( --footer-height ) - .65rem );
	padding: 0 .8rem;
	border: 1px solid var( --rule );
	border-radius: 999px;
	background: var( --card-background );
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-xs );	/* xs, not sm: three pills share a phone-wide footer and the counts must not truncate */
	cursor: pointer;
	box-shadow: 0 1px 1px rgba( 0, 0, 0, .05 );
}

/* Border and ground only, never the text colour: a hover that inked the text would repaint a
   warning-toned reading plain at the moment somebody reached for it. */
.app-footer .status-tray-toggle:hover
{
	border-color: var( --ink-quiet );
	background: color-mix( in srgb, var( --ink ) 4%, var( --card-background ) );
}

.app-footer .status-tray-toggle:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }

.app-footer .status-tray-toggle[data-tone="accent"] { border-color: color-mix( in srgb, var( --accent ) 45%, var( --rule ) ); }
.app-footer .status-tray-toggle[data-tone="warning"] { border-color: color-mix( in srgb, var( --warning ) 45%, var( --rule ) ); }

/* THE WEATHER on the queue button (D662): its ground from cool to hot as the work per slot rises. The tints
   deepen through three levels and the fourth is a solid dark red with white text; calm (0) is the plain
   button. After the tone rules so the weather's border wins over the running accent, and before nothing
   that would override it -- a warning still shows as the badge inside the button. */
.app-footer .status-tray-toggle[data-weather="1"] { background: color-mix( in srgb, var( --weather-1 ) 14%, var( --card-background ) ); border-color: color-mix( in srgb, var( --weather-1 ) 55%, var( --rule ) ); }
.app-footer .status-tray-toggle[data-weather="2"] { background: color-mix( in srgb, var( --weather-2 ) 22%, var( --card-background ) ); border-color: color-mix( in srgb, var( --weather-2 ) 60%, var( --rule ) ); color: var( --ink ); }
.app-footer .status-tray-toggle[data-weather="3"] { background: color-mix( in srgb, var( --weather-3 ) 32%, var( --card-background ) ); border-color: var( --weather-3 ); color: var( --ink ); }
.app-footer .status-tray-toggle[data-weather="4"] { background: var( --weather-4 ); border-color: var( --weather-4 ); color: #fff; }
.app-footer .status-tray-toggle[data-weather="4"] .status-dot { background: #fff; }
/* The weather changes slowly (Jon, 2026-10-08: "a slowish transition so it doesn't get all blinky"): a poll
   every six seconds can step the level up and back, and a two-second ease turns that into a drift rather
   than a flicker. Instant for anybody who has asked their system for less motion. */
.app-footer .status-tray-toggle[data-status="queue"] { transition: background-color 2s ease, border-color 2s ease, color 2s ease; }
@media ( prefers-reduced-motion: reduce ) { .app-footer .status-tray-toggle[data-status="queue"] { transition: none; } }

.app-footer .status-tray-toggle[aria-expanded="true"]
{
	background: color-mix( in srgb, var( --ink ) 9%, var( --card-background ) );
	border-color: var( --ink-quiet );
	box-shadow: inset 0 1px 2px rgba( 0, 0, 0, .14 );
}

/* The reading is the part that gives when the row runs out -- a phone with two busy trays, or three
   buttons once the Shelf arrives. The dot and the badge are the at-a-glance half and never shrink. */
.app-footer .status-tray-toggle [data-status-text]
{
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

.app-footer .status-tray-toggle .status-dot,
.app-footer .status-tray-toggle .status-attention
{
	flex: none;
}

/* The invitation's picker is a <label> wrapping a hidden input. It wears .control-pill rather than
   .badge, because it is a control: under the rule above, a thing you press is round. It needed a
   cursor and a focus ring bolted on when it was a badge, which was the signal that the shape was
   wrong (D129). */

/* ---------------------------------------------------------------------------------------------
   PHP diagnostics: a construction-site instrument.

   Loud on purpose while this is a development environment. It is the runtime reporting a defect
   nobody had to write a test for, and the whole reason it exists is that two such reports were
   missed in one day (D116). When display_errors is inverted for production this goes with it.
   --------------------------------------------------------------------------------------------- */

.status-diagnostics
{
	border: 0;
	font: inherit;
	cursor: pointer;
	background: none;
	color: inherit;
	padding: 0;
	text-decoration: underline dotted;
	text-underline-offset: .2rem;
}

.diagnostics-panel
{
	position: fixed;
	left: 1rem;
	right: 1rem;
	bottom: calc( var( --footer-height ) + 1rem + env( safe-area-inset-bottom, 0px ) );
	z-index: 40;
	max-height: min( 50vh, 26rem );
	display: flex;
	flex-direction: column;
	background: var( --card-background );
	/* Tokens, not literals (B7): the pink border is the danger tone mixed into the card, the way the
	   warning pill's hover does it, so a theme that moves --danger moves this panel with it. */
	border: 1px solid color-mix( in srgb, var( --danger ) 40%, var( --card-background ) );
	border-left: 3px solid var( --danger );
	border-radius: .6rem;
	box-shadow: 0 .6rem 2rem rgba( 0, 0, 0, .24 );
	overflow: hidden;
}

.diagnostics-panel[hidden] { display: none; }

.diagnostics-head
{
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: .45rem .7rem;
	border-bottom: 1px solid var( --rule );
}

.diagnostics-title { font-size: var( --text-sm ); font-weight: 600; color: var( --danger ); }
.diagnostics-actions { display: flex; gap: .4rem; }

/* Round is pressable (D129), and that rule does not stop applying because a control is small: these
   two were the panel's own miniature buttons with a .3rem radius, which read as a third shape
   beside the badge's square corner and the pill's round one. Same pill, less of it. */
.diagnostics-actions button
{
	border: 1px solid var( --rule );
	border-radius: 999px;
	background: var( --page-background );
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-xs );
	padding: .1rem .5rem;
	cursor: pointer;
}

.diagnostics-actions button:hover { border-color: var( --ink-quiet ); color: var( --ink ); }

.diagnostics-list
{
	margin: 0;
	padding: .5rem .7rem .7rem 1.8rem;
	overflow-y: auto;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: var( --text-xs );
	line-height: 1.5;
	color: var( --ink );
}

/* Each entry keeps its own line breaks and stays inside the panel: a PHP warning quotes whatever
   provoked it, so these strings can be long and can contain anything. */
.diagnostics-list li { margin: 0 0 .35rem; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------------------------
   The publish panel (§9.6). Shell-only: /item.php is the surface machines fetch and carries no
   publish control, so nothing here is loaded by that page.

   Every selector is scoped under .share-panel by name. This file made the item page unscrollable
   once by reaching outside what it owns, and item.css then capped the whole application at 58rem;
   the rule that came out of it is that a globally loaded stylesheet may only style what it owns.
   --------------------------------------------------------------------------------------------- */

/* No margin and no rule of its own since D237: the panel is a child of the item view's two-column
   grid, and the box around it -- border, padding, ground -- is drawn by that grid in item.css so
   that this panel and the title-and-description box beside it cannot disagree about their edges.
   It kept a top border here for as long as it was appended below the whole page. */
.share-panel
{
	margin: 0;
}

/* THE PUBLIC ROW (D331) is an .action-row -- ui-design.md §4.2's shape, general classes from the
   item page. What this block adds is only what the row carries that the lifecycle rows do not:
   the address field under the detail, and a downloads checkbox beside the verb. The address reuses
   the link rows' three-layer copy control by class name, so one rule set styles a link's URL and
   the public address alike. */
/* No box of its own (Jon, 2026-09-05): the row's title sits on the panel's own left edge, under the
   summary, like any other action row on the item page. The state is said by the title and by the
   badge beside the item's name, not by a coloured border. */
.share-panel .share-public-row
{
	margin: 0 0 1.25rem;
	padding: 0;
	border: 0;
	align-items: flex-start;
}

/* LAST IN THE PANE, UNDER A HAIRLINE (Jon, 2026-09-22): openness grows downwards, so the item page
   and the folder's disclosure both draw the links and the form that makes one first and the public
   offer last. Following a form rather than the summary, the row takes the rule that every other
   tool area draws between its offers, and the margin that used to sit under it moves above.
   Adjacent-sibling rather than a class, so the rule is a fact about the order and cannot be left
   on a row that moves back up. */
.share-panel :is( .share-create, .share-list, .empty-state ) + .share-public-row
{
	margin: 1.25rem 0 0;
	padding-top: 1rem;
	border-top: 1px solid var( --rule );
}

/* The list under the form since 2026-09-26 (Jon): the form's row, then the heading over what exists. */
.share-panel .share-create + .share-list-heading { margin-top: 1.25rem; }

/* The dock that closes a scope-bar disclosure (Jon, 2026-09-22): Done under the share panel,
   Cancel and Save under the description. In the share panel it follows the public row, whose
   bottom margin the rule above took away, so it spaces itself; in the describe form the grid gap
   already does. `.button-dock` is the §4.3 dock and says where the default sits. */
.share-panel > .scope-panel-dock { margin-top: 1.25rem; }

.share-panel .share-public-row .share-public-address { margin-top: 0.5rem; }

/* TWO CONTROLS IN ONE ROW ARE A COLUMN, the act on top (Jon, 2026-09-05). Side by side, the
   secondary control pushed the main button off the row's right edge and the pair read as neither
   a button nor a form. Stacked, the verb keeps the column's edge and the downloads choice -- or
   the Open link -- sits under it, where a footnote to the act belongs. This is the first row on
   the page with two controls and the shape is meant to be reused. */
.share-panel .share-public-controls
{
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 0.5rem;
	flex: 0 0 auto;
}

.share-panel .share-public-controls .control-pill
{
	min-width: 9.5rem;
	justify-content: center;
	text-align: center;
}

.share-panel .share-public-controls .share-public-downloads,
.share-panel .share-public-controls .share-public-ai-training-choice
{
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	font-size: var( --text-base );
	white-space: nowrap;
}

/* The AI-training sentence on the public state (D347): under the address, quiet, one line or two. */
.share-panel .share-public-ai-training { font-size: var( --text-base ); margin: 0.35rem 0 0; }

/* The heading over the links: the same weight as an action-row title, so the public row's title and
   this read as two headings of one panel rather than a heading and a row. */
.share-panel .share-list-heading
{
	margin: 0 0 0.6rem;
	font-size: var( --text-base );
	font-weight: 600;
}

/* The "No links yet" box takes the list's margins, not a paragraph's (Jon, 2026-09-22): with the
   browser's own 1em above it, the heading floated between the segment above and its own box and
   read as a footer to the wrong one. Under the heading's 0.6rem, it is the heading's box. */
.share-panel .share-list-heading + .empty-state { margin: 0 0 1.25rem; }

/* Open beside Withdraw (Jon, 2026-09-05): the two controls of a link row on one line, the link
   first because looking precedes withdrawing. A flex row rather than the form alone so that the
   anchor and the form's button size themselves from the same pill rule and sit on one baseline. */
.share-panel .share-row-actions
{
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	align-items: center;
}

.share-panel .share-row-actions .share-revoke { display: contents; }

/* No heading rule of its own since D237: the panel is a .tool-area box like the surfaces above it,
   and its heading is a .tool-area-title -- one definition, so this box and the description box
   beside it cannot disagree about the size of a word. This rule used to say 1.05rem and would have
   WON on specificity (`.share-panel h2` over `.tool-area-title`), which is how one of a matched pair
   ends up a shade larger than the other for a session or two before anybody notices. */

.share-panel .share-list { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.85rem; }

.share-panel .share-row
{
	display: grid;
	gap: 0.4rem;
	padding: 0.75rem;
	border: 1px solid var( --rule, #8884 );
	border-radius: 0.3rem;
}

.share-panel .share-row-head { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: baseline; font-size: var( --text-base ); }
.share-panel .share-scope { font-weight: 600; }
.share-panel .share-download { opacity: 0.7; }

/* Readonly and full width: a share link that cannot be copied in one gesture is a share link
   somebody will retype and get wrong. Since D237 the value is the WHOLE link, so `text-overflow`
   earns its place -- a token plus an origin is wider than half a column, and a field that scrolls
   its own text is still one click from being copied whole. */
.share-panel .share-url-row
{
	display: grid;
	gap: 0.2rem;
}

.share-panel .share-url
{
	width: 100%;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: var( --text-base );
	padding: 0.4rem 0.5rem;
	text-overflow: ellipsis;
	cursor: pointer;
}

/* The hint doubles as the confirmation, which is why it is a live region in the markup: "Copied"
   replaces "Click to copy" in place rather than appearing somewhere else on the page. The
   copied state is set by item.js and cleared by it a few seconds later. */
.share-panel .share-url-hint
{
	font-size: var( --text-sm );
	opacity: 0.7;
}

.share-panel .share-url-hint[ data-copied="true" ]
{
	opacity: 1;
	font-weight: 600;
	color: var( --accent, #2f6f4f );
}

/* The embed snippet in a renditions tool area (D332): the share link's copy control, with a
   textarea where the link had an input, because the markup is several lines a person reads before
   pasting. Same hint, same live-region confirmation, same item.js handler. `wrap="off"` in the
   markup and `overflow: auto` here keep the srcset entries on their own lines rather than folding
   a long URL into something that no longer looks like the markup it is. */
.embed-snippet
{
	display: grid;
	gap: 0.3rem;
	margin-top: 0.8rem;
}

.embed-snippet-title
{
	margin: 0;
	display: grid;
	gap: 0.1rem;
}

.embed-snippet-source
{
	width: 100%;
	box-sizing: border-box;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: var( --text-sm );
	line-height: 1.35;
	padding: 0.4rem 0.5rem;
	overflow: auto;
	white-space: pre;
	resize: vertical;
	cursor: pointer;
}

.embed-snippet .share-url-hint
{
	font-size: var( --text-sm );
	opacity: 0.7;
}

.embed-snippet .share-url-hint[ data-copied="true" ]
{
	opacity: 1;
	font-weight: 600;
	color: var( --accent, #2f6f4f );
}

/* A gallery's ground, as tabs over the one field (D506): joined segments sitting on the field's
   top edge, the chosen one filled -- the toolbar's layout control's language (--accent-quiet fill,
   --rule border), so it reads as a choice of view and not as three actions. */
.embed-ground-tabs
{
	display: flex;
	flex-wrap: wrap;
	margin-top: 0.3rem;
}

.embed-ground-tab
{
	font: inherit;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
	background: transparent;
	border: 1px solid var( --rule );
	padding: 0.3rem 0.75rem;
	cursor: pointer;
}

.embed-ground-tab + .embed-ground-tab
{
	margin-left: -1px;
}

.embed-ground-tab:first-child
{
	border-radius: 0.42rem 0 0 0.42rem;
}

.embed-ground-tab:last-child
{
	border-radius: 0 0.42rem 0.42rem 0;
}

.embed-ground-tab[ aria-selected="true" ]
{
	background: var( --accent-quiet );
	color: var( --ink );
	font-weight: 600;
}

/* Inside the same grid as the snippet would be, under the same heading, so no top margin of
   its own: the grid's gap places it. */
.embed-snippet-absent
{
	margin: 0;
	font-size: var( --text-base );
}

/* Four quadrants on two shared lines: a caption line, and a control line under it.
   
   The form used to state only `align-items: end`, which aligns the BOTTOMS of the four items and
   lets everything above them land where it may -- and a <select>, a text <input>, a checkbox and a
   <button> each size themselves from their own font and their own chrome, so no two of them are the
   same height. The difference was therefore paid at the top, where the words are: "Link name" sat a
   few pixels above "Covers", the checkbox floated on a line of its own with "Allow downloads"
   beneath it, and the button sat lower than both. Four controls, four baselines (Jon, 2026-09-03).
   
   So the control row is given ONE height, stated here and used by every quadrant. The caption row is
   a single line of one size everywhere, so once the control row agrees the two lines agree, and the
   form reads as a row of fields rather than as four controls that happen to be adjacent. */
.share-panel .share-create
{
	/* The shared token, not a number of its own. It was 2.35rem, measured by eye against the text
	   input beside it, and that is what made the share panel's submit the tallest button on the
	   page. Both sides name one number now. */
	--share-control-height: var( --control-height );

	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem 1rem;
	align-items: end;
}

/* `> label` rather than `label`, and two named rows rather than `gap` alone: the explicit second row
   is what a <select> and an <input> are stretched INTO, which is the height match the eye reads.
   
   `minmax( …, auto )` rather than the bare height, and it is a GUARD rather than a tuning (Claude,
   2026-09-21, on Jon's report that this one control was clipped): a bare row height is a ceiling as
   well as a floor, and a control squeezed under its
   own minimum does not shrink, it CLIPS. The <select> was drawn with the descenders of "This item
   only" cut off, because --control-height is 1.8rem here and a 14.4px option on the page's
   `line-height: 1.5` wants 21.6px of line plus its padding and borders -- 36.4px asked, 28.8px
   given. Everywhere else in this stylesheet the system height is spent as `min-height`; this was
   the one place that spent it as an exact height, which is why this was the one control that
   clipped. With the block below the row never has to grow, so the four quadrants still share one
   line; the minmax is what stops a smaller token from clipping instead of growing. */
.share-panel .share-create > label
{
	display: grid;
	grid-template-rows: auto minmax( var( --share-control-height ), auto );
	gap: 0.25rem;
	font-size: var( --text-base );
}

/* The checkbox and its word are the control row of the Downloads quadrant, so they are a span inside
   the label rather than the label itself.
   
   The rule this replaces tried to say the same thing as `.share-download-choice { display: flex }`
   and lost on specificity to `.share-create label`'s grid -- two classes against two classes and an
   element -- which is exactly why the box was drawn ABOVE its own caption in the screenshot that
   started this. A selector that has to win an argument with the rule three lines above it is a
   selector that will lose it again; the span cannot collide with a label rule at all. */
.share-panel .share-create .share-checkbox-row
{
	display: flex;
	align-items: center;
	gap: 0.4rem;
}

/* The name is the quadrant that takes the slack.  It is the only one of the four whose width is
   arbitrary: the select is as wide as its longest option, and the checkbox and the button are as
   wide as their words, so shrinking any of those costs something. `min-width: 0` because a flex item
   refuses to shrink below its content otherwise, and the content here is a text input at its default
   size -- without it the row simply wrapped instead, which put the button on a line of its own the
   moment the panel was narrower than four intrinsic widths, which is every time it is drawn in the
   two-column band beside title-and-description. */
.share-panel .share-create > label:first-of-type
{
	flex: 1 1 8rem;
	min-width: 5rem;

	/* Capped, or the folder panel -- which has no Covers select and 46rem to spend -- draws a field
	   half a metre wide for the words "Client delivery". Growing into the slack is the point; taking
	   all of it is a field that reads as the panel rather than as one of its quadrants. */
	max-width: 20rem;
}

.share-panel .share-create > label:first-of-type input[ type="text" ] { width: 100%; min-width: 0; }

/* border-box so that the stated row height is the height the border is drawn at, rather than that
   height plus two borders and the padding -- which would put the three controls back out of step by
   a couple of pixels each, differently.
   
   `line-height` and the block padding are what these controls ASK FOR, and asking for less than the
   row offers is the other half of the fix above (Claude, 2026-09-21). A form control inherits the
   page's 1.5, which is a reading measure for prose and far more than a single line of chrome needs:
   at --text-base it alone is 21.6px, and with 0.4rem above and below plus two borders the control
   wanted 36.4px inside a 28.8px row. 1.2 and 0.25rem bring the ask to 27.3px, under the row at
   today's token with a little to spare. None of it is visible while the row is taller than the ask,
   which is the point -- the control is stretched to the row either way, and symmetric padding keeps
   the text centred, so this changes nothing at app.css's own 2.35rem and stops the clip at 1.8rem
   (checked in Chrome with experiments.css both on and off). */
.share-panel .share-create input[ type="text" ],
.share-panel .share-create select
{
	padding: 0.25rem 0.5rem;
	line-height: 1.2;
	box-sizing: border-box;
}

/* The right-hand room is where the element rule's chevron stands; the padding above would have put
   the last word of "Contributor -- may upload, edit and share" underneath it. */
.share-panel .share-create select { padding-right: 1.9rem; }

/* The submit is the fourth quadrant, and it has no caption: `align-items: end` on the row puts its
   bottom on the same line as the other three, and the shared height then puts its top there too. It
   is a .control-pill now (it was a bare <button>, drawn in platform blue by the browser -- the same
   defect .details-save was written to fix, in the one form that had not been given a class yet). */
.share-panel .share-create > button
{
	height: var( --share-control-height );
	padding-inline: 0.9rem;
	justify-content: center;
	/* The same width and the same edge as Make public below it (Jon, 2026-09-26): the public row's
	   control column sets 9.5rem and sits at the panel's right; this button matches both, so the
	   two verbs of the box line up. */
	min-width: 9.5rem;
	margin-left: auto;
}


/* ---------------------------------------------------------------------------------------------
   The details form: title and description (D141).

   It borrows .tool-area-header from the areas above it deliberately -- it IS a tool area in every
   sense except that its tool is words rather than a chain, and giving it its own heading vocabulary
   would say it is a different kind of thing than it is.

   The single-column grid is not a layout preference: a title is one line and a description is
   several, so side-by-side would give the short field the same width as the long one and read as a
   form designed by nobody in particular. `max-width` on the fields rather than on the section, so
   the heading and the summary keep the measure the rest of the page uses.
   --------------------------------------------------------------------------------------------- */

.details-area { margin: 1.5rem 0; }

/* ---------------------------------------------------------------------------------------------
   The scope action bar (D204): what can be done TO the team, project or folder being looked at.

   A flex row rather than a grid, because the controls differ per scope -- a team has no Remove, an
   Inbox has neither Rename nor Remove -- and a grid would leave the gaps where the missing ones
   would have been. `flex-wrap` because "New project · People and grants · Rename… · Remove…" is
   wider than a narrow window and wrapping is better than a scrollbar for four buttons.

   The disclosures in this bar are NOT <details> -- they are a checkbox, a label and a panel, and
   the block below says why at length. The `.details-area > summary` rules further down still serve
   the disclosures on other pages, which are still <details>; nothing in this bar reaches them.

   `.scope-actions` is the LAYOUT, and six elements wear it. Each also carries a name of its own
   (Jon, 2026-09-07), so a rule can reach one without reaching the rest:

     .scope-actions-bar               the scope page's bar, plus .scope-actions-bar-<team|project|folder>
     .scope-actions-person            the person page's bar (the pin control alone)
     .scope-actions-search            the search page's bar (D371, the pin control alone -- save
                                      this search), which needs no rules of its own and is named
                                      here because an unnamed member of this layout is how the
                                      inventory above stopped being true the first time
     .scope-actions-create            the create form's Cancel / Create dock (also .button-dock)
     .scope-actions-action-switch     the Activate / Deactivate form on a folder-action card
   --------------------------------------------------------------------------------------------- */

.scope-actions
{
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	align-items: center;
	margin: 0.75rem 0;

	/* The positioning context for the disclosure checkboxes below, which are placed out of the flex
	   flow so they take no gap while staying focusable. */
	position: relative;
}

/* **The rules below are what makes the paragraph above true**, and until now it was not: that
   comment named Rename and Remove as members of this row while index.php closed the row after three
   pills and appended each disclosure as a block-level `.details-area` sibling, 1.5rem of margin
   apiece. Three closed buttons occupied a third of the viewport to say three words. Every control
   is a child of the bar now, and a closed disclosure costs exactly what a pill costs, because it IS
   one. */

/* A DISCLOSURE IN THIS BAR IS A PILL AND A MODAL DIALOG (D493). index.php emits three ADJACENT
   siblings -- a checkbox, its label, a <dialog class="scope-panel"> -- and app.js answers a checked
   box with showModal(), so the panel opens in the middle of the window over a backdrop that blocks
   the page (D489's model for the folder-action panes). The panel is in the top layer, so it takes no
   room in the row: the row is only ever a row of pills.

   **What this replaced**, in one sentence, because it is why the rules below are short: the panel
   used to open in the flow beneath the whole row, which took a flex basis, an `order`, and a phone
   exception that made the scrolling strip wrap again while anything was open -- and before that,
   three failed attempts to do the same with <details>, the last of which looked right in Chrome and
   was broken in Safari (WebKit ignores `display: contents` on an element with a UA shadow tree).

   The frame is the action panes' frame: no padding, so a click that reaches the dialog itself is a
   click on the backdrop (app.js closes on it); the tool area inside is the box, and scrolls when it
   is taller than the window. */

dialog.scope-panel { width: min( 44rem, calc( 100vw - 2rem ) ); max-height: calc( 100dvh - 3rem ); padding: 0; border: 0; border-radius: 10px; background: transparent; color: var( --ink ); overflow: visible; }
/* A literal, not a token: ::backdrop inherits nothing in the Chrome this host runs (D489). */
dialog.scope-panel::backdrop { background: rgba( 16, 18, 20, .45 ); }
dialog.scope-panel > .tool-area { max-height: calc( 100dvh - 3rem ); overflow-y: auto; box-sizing: border-box; overscroll-behavior: contain; }

/* A DIALOG COVERED BY ONE IT OPENED (D494) stays open, with its edits, and is hidden until the one
   on top closes; its backdrop goes too, so the page is dimmed once rather than twice. */
dialog[data-dialog-covered] { visibility: hidden; }
dialog[data-dialog-covered]::backdrop { background: transparent; }

/* A pane drawn on a page only so another pane can chain to it (the actions page's Share folder): the
   checkbox and pill are there for app.js, not for a person, whose way in is the chaining control. */
.dialog-chain-host > .scope-panel-toggle,
.dialog-chain-host > .control-pill { display: none; }

/* Which pill opened the panel, said on the pill -- visible past the backdrop, and still the answer
   to "where did that come from" when the panel closes and focus lands back on it. */
.scope-panel-toggle:checked + .control-pill
{
	border-color: var( --accent );
	color: var( --accent );
}

/* The checkbox is the control's real state and its real tab stop, so it is hidden without being
   removed: `position: absolute` takes it out of the flex flow (a `display: none` checkbox is not
   focusable, and a checkbox that is not focusable is a disclosure no keyboard can open). It sits
   immediately before its own label, so the tab order is the reading order, and the ring is drawn on
   the pill because that is the thing a person can see. */
.scope-panel-toggle
{
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

.scope-panel-toggle:focus-visible + .control-pill
{
	outline: 2px solid var( --accent );
	outline-offset: 2px;
}

/* A scope-bar disclosure whose body is a TOOL AREA (Jon, 2026-09-07): every panel wraps itself in
   the same box as the share panel, because open, the description panel was a textarea, a cheat
   sheet, a keyword field and a button standing loose with nothing saying they were one thing. Two
   rules for any panel that does: the box is the whole dialog, so it drops its 1.5rem page-flow
   margin; and the form takes the box's width, because .details-form's 46rem reading measure inside
   the box leaves the border carrying on past the Save button. */
.scope-panel > .tool-area { margin: 0; }
.scope-panel > .tool-area > .details-form { max-width: none; }

/* A notice standing in for a control (the Inbox explanation, a removal refusal) takes a line of its
   own after everything, and drops the trailing margin it wears as a paragraph: here it is a row of
   the bar, not a paragraph following one. */
.scope-actions > .notice { flex-basis: 100%; margin: 0; order: 2; }

/* The bar's icons (D120's drawn set, not typed glyphs). A shade under the pill's own cap height:
   .control-pill is .78rem text, and a 1rem icon beside it reads as an icon with a caption. */
.scope-actions .control-pill .icon { --icon-size: 0.85rem; }

/* ---------------------------------------------------------------------------------------------
   ON A PHONE THE BAR STAYS ONE ROW AND SCROLLS SIDEWAYS (Jon, 2026-09-11).

   `flex-wrap` above was chosen for a narrow *window*, where wrapping four pills beats a scrollbar.
   On a phone the same bar is eight pills deep -- New folder, People and grants, Describe, Pin,
   Mention, Share, Rename, Move, Archive, Remove -- and wrapped it is a block of buttons three rows
   high standing between the title and the content. One row that scrolls under a thumb is what a
   phone does with a toolbar, and the row bleeds to the screen's edges so the last visible pill is
   cut mid-word, which is the scroll hint. No scrollbar is drawn: the row scrolls by touch, and a
   bar under four pills is noise.

   **The row no longer wraps while a panel is open** (D493). It used to, because the panel was a
   flex child taking a whole line under the pills, and a line under the pills only exists in a
   wrapping row. The panel is a modal dialog now, in the top layer, so the strip stays a strip.

   Bars carrying a notice (an Inbox's explanation, an Outbox's state sentence) keep wrapping too: a
   sentence is not a pill, and `flex-basis: 100%` in a scrolling row would put it a screen to the
   right. The item toolbar is the same bar under another name, with one more full-width child, the
   replace-source progress card, which upload.js unhides when a file is chosen.

   `:has()` has been in every evergreen engine since 2023; an older browser ignores the whole rule
   and wraps as before, which is the correct fallback. Pills must not shrink to fit -- `flex: 0 0
   auto` -- or the row would squeeze them instead of scrolling.
   --------------------------------------------------------------------------------------------- */

@media ( max-width: 46rem )
{
	.scope-actions-bar:not( :has( > .notice ) ),
	.item-toolbar:not( :has( .replace-cards:not( [hidden] ) ) )
	{
		flex-wrap: nowrap;
		overflow-x: auto;
		overscroll-behavior-x: contain;
		scrollbar-width: none;
		margin-inline: -1rem;
		padding-inline: 1rem;
		/* Room for a pill's focus ring, which `overflow` would otherwise clip top and bottom. */
		padding-block: 3px;
	}

	.scope-actions-bar:not( :has( > .notice ) )::-webkit-scrollbar,
	.item-toolbar:not( :has( .replace-cards:not( [hidden] ) ) )::-webkit-scrollbar
	{
		display: none;
	}

	/* NO SCROLL SNAP, and it was tried. The item toolbar's first child is the pin form, which is
	   `display: contents` and so has no box to snap to; the first snap point was the second pill, and
	   Chrome re-snaps at layout to the nearest position -- which, for a row only a little wider than
	   the screen, was the END. The page opened with the first pill cut off at the left edge. A snap
	   point set that depends on which child happens to have a box is a hazard, not a nicety. */
	.scope-actions-bar > *,
	.item-toolbar > *,
	.item-toolbar .item-replace-source > .control-pill
	{
		flex: 0 0 auto;
	}
}

.details-area > summary { cursor: pointer; display: inline-block; list-style: none; }
.details-area > summary::-webkit-details-marker { display: none; }

/* A <summary> WEARING .control-pill IS STILL A PILL, and the rule above was quietly taking that
   away: `.details-area > summary` is two classes and an element, so it outranks `.control-pill` and
   replaced its `inline-flex` with `inline-block`. Nothing looked wrong while the pill held one word
   -- a block and a flex row draw a lone label identically -- and the day "Add somebody…" gained a
   `+` in front of it, the flex `gap` that was supposed to separate them did nothing, because there
   was no flex container left to have a gap. It rendered "+Add somebody…", found by looking at a
   screenshot rather than by any assertion (2026-09-04).

   `list-style` and the marker stay suppressed above; this restores only the display, which is the
   one declaration of the pill's that the disclosure rule never meant to overrule. */
.details-area > summary.control-pill { display: inline-flex; }

/* Open, the panel sat directly on the pill that opened it: .details-form has no top margin and
   <details> puts none between its two children. The gap the pill wanted (Jon, 2026-09-05). */
.details-area[open] > summary { margin-bottom: 0.75rem; }


/* A disabled control still has to be readable -- it is carrying the reason it is disabled in its
   own tooltip (invariant 16), and a control faded to the point of being missed cannot do that. */
.control-pill:disabled { opacity: 0.55; cursor: not-allowed; }

/* THE COMPACT PILL, and it was a class in the markup that no stylesheet defined -- archive.php has
   rendered `control-pill-compact` since D276 and it has never drawn anything. It is the deliberate
   exception to --control-height: a control riding inside a dense row (an item card, a list line)
   where a full-height pill would set the row's height. Everything else about it is the pill, so a
   compact control is still visibly the same kind of thing. */
.control-pill-compact
{
	min-height: 0;
	padding: .12rem .45rem;
	font-size: var( --text-xs );
}

.details-form
{
	display: grid;
	gap: 0.75rem;
	max-width: 46rem;
}


/* ---------------------------------------------------------------------------------------------
   THE BUTTON DOCK, as a class rather than as a shape three surfaces each rebuild (ui-design.md
   §4.3, extended 2026-09-04).

   §4.3 was written from the sign-in page and said it held nowhere else. It now holds in three
   places -- the sign-in card, the move interstitial and the create form -- and this is the one
   declaration all three share: the pair sits at the RIGHT-HAND end, the default is rightmost, and
   source order is visual order is tab order, so there is no `order` property anywhere near it.

   `flex-end` and nothing else. It deliberately does not set the tone, the fill or the width: which
   button is the default is a fact about the surface (and on the sign-in page it MOVES at runtime),
   so a dock that decided it would be a dock that fights signin.js.
   --------------------------------------------------------------------------------------------- */
.button-dock
{
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	align-items: center;
	gap: .5rem;
}

/* A confirm button is wider than its word (Jon, 2026-10-10). "Done" and "Save" set the width of the
   most consequential control on their panel from the length of a four-letter verb, which made the
   press that commits a form the smallest target on it. A floor, not a width: the pill already
   centres its label, so a short verb gains ground on both sides and a long one ("Erase permanently")
   is untouched. Scoped to the surfaces where a pill IS a confirm -- a dock, the move interstitial,
   the membership Save, anything wearing .details-save (the project's "this pill is a Save" class:
   the item's Title and description panel, every folder action's verb), and the promoted default
   anywhere -- rather than to every pill, because a toolbar pill's width saying exactly its word is
   what lets a bar hold a dozen of them. The compact pill is excluded for the same reason it
   exists: it rides in rows whose height and width it must not set. */
.button-dock .control-pill:not( .control-pill-compact ),
.interstitial-form .control-pill,
.membership-save .control-pill,
.control-pill.details-save,
.control-pill[data-default="true"]
{
	min-width: 6rem;
}


/* (The equal-width rule for the account panel's three stacked credential forms went on 2026-09-24:
   they are one row now, `.user-panel-actions` at the end of this file, and nothing else matched it.) */

/* ---------------------------------------------------------------------------------------------
   A details panel whose content is a TABLE, and therefore is not prose (D293).

   The 46rem above is a measure for reading -- a title, a description, a few labelled fields -- and
   it is right for every panel that holds those. It was silently wrong for the two panels that hold
   a data table: on the people and grants surfaces the members table ran the full width of the view
   at 1322px while the "Add someone" table directly beneath it, being inside a .details-form, was
   cut to 736px and grew its own horizontal scrollbar. Two tables of the same columns, one above the
   other, at half the width -- reported by Jon on 2026-09-03 and measured in Chrome at 1680px before
   this rule existed.

   `max-width: none` rather than a larger number, because the container is already the measure: the
   view's own column is what stops it, exactly as it stops the table above.
   --------------------------------------------------------------------------------------------- */

.details-form-wide { max-width: none; }

/* The disclosure itself is already full width -- <details> is a block -- so this exists only to say
   so where the class is read, and to give the pair a name that travels together. */
.details-area-wide { max-width: none; }

/* ---------------------------------------------------------------------------------------------
   The membership surface: one form, one Save (D293).

   The Save sits AFTER the picker rather than beside the members table, because it applies to both
   and a control placed beside one of two things it acts on says it acts on that one. A top border
   for the same reason a form's footer usually has one: it marks the end of the things being decided
   and the beginning of the press that commits them.
   --------------------------------------------------------------------------------------------- */

.membership-save
{
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.6rem 1rem;
	margin-top: 1.25rem;
	padding: 1rem 0 0.75rem;
	border-top: 1px solid var( --rule );

	/* **Sticky, and this is the measurement that asked for it.** With the picker open on a 963px
	   window the Save was 470px below the fold, behind ten candidate rows -- so the one control that
	   commits everything a person has ticked was the one control they could not see while ticking.
	   Shortening the page would not fix it: the members table and the picker's own tools already
	   reach 495px before the first candidate, so the button clears the fold only with a list of two.

	   `bottom: 0` sticks it to the foot of the scrolling region for exactly as long as the form's box
	   is crossing it, and returns it to the flow at the form's end -- so it is never a bar floating
	   over an unrelated part of the page, which is what a fixed footer would have been. It needs an
	   opaque background for the same reason it needs the border: rows scroll underneath it. */
	/* Not `bottom: 0`, which stuck it 3rem short of the foot of the region with a row of the table
	   showing underneath (Jon, 2026-09-05): the sticky rectangle is the scrollport less the scroll
	   container's padding, in every engine, so the true bottom is that padding further down. The
	   region names the number (.app-main); the fallback is for a form that is ever rendered outside
	   it. The bar still never leaves its form -- a sticky box is confined to its containing block --
	   so at the form's end it returns to the flow as before. task38 pins the pair of declarations. */
	position: sticky;
	bottom: calc( -1 * var( --main-padding-bottom, 0rem ) );
	background: var( --page-background );
	z-index: 1;
}

.membership-save .details-description { margin: 0; }

/* The Save breathes once there is something for it to save -- a role changed, or somebody ticked --
   on the interface's one heartbeat (--attention-rhythm), set by app.js as data-attention. It is the
   same statement the footer's status dot makes: something here wants you. Still, until then. */
.membership-save [data-membership-save][data-attention="true"]
{
	animation: attention-pulse var( --attention-rhythm ) ease-in-out infinite;
}

@media ( prefers-reduced-motion: reduce )
{
	.membership-save [data-membership-save][data-attention="true"] { animation: none; }
}

/* The members table's own rows carry controls, and a control sits on the row's middle, not its
   top: with `vertical-align: top` the name and the badge rode the top edge of a cell the select had
   made 2.35rem tall. Scoped to this form; the matrix and the shares table are text and keep `top`. */
.membership-form .data-table td { vertical-align: middle; }

/* The cell with Remove in it takes only the button's width and never wraps it. */
.member-remove { width: 1%; white-space: nowrap; }

/* The lockout caution, as a row of its own beneath the member it is about (index.php says why it
   moved out of the Remove cell). No top rule and no top padding, so it reads as a footnote to the
   row above rather than as a row in its own right -- which is also why it does not take the hover
   tint on its own: pointing at the caution is pointing at its member. */
.member-caution > td
{
	border-top: 0;
	padding-top: 0;
	font-size: var( --text-sm );
	color: var( --warning );
}

.data-table tbody tr.member-caution:hover > td { background: transparent; }

/* The revoke forms are markup with nothing to draw: their buttons live in the table and reach them
   by `form=`. Not `display: none`, which would be equivalent here and would stop being equivalent
   the moment one of them grew a field. */
.member-revoke-form { display: contents; }

/* A role select in a table cell should not stretch the column to the width of its longest option
   sentence -- "Contributor — may upload, edit and share" is a paragraph. */
.member-role-select { max-width: 20rem; }

/* The picker's tools: search, the ticked count, and the pager. Rendered `hidden` by index.php and
   revealed by app.js, so a reader with no script sees the whole list and no controls that would do
   nothing -- the same "absent rather than disabled" rule the rest of this interface follows. */
.picker-tools
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem 1rem;
}

/* `.details-form .picker-search`, two classes, for the reason `.mention-form .mention-candidate`
   gives further down: this label sits inside a .details-form, whose `label { display: grid }` rule
   (0,1,1) beat a bare `.picker-search` (0,1,0) and stacked "Find somebody" ABOVE the field -- so the
   field, centred as a pair with its caption, sat half a line below the Clear pill beside it (Jon,
   2026-09-05). Caption and field on one line, and the two controls line up on the system height. */
.details-form .picker-search
{
	display: flex;
	align-items: center;
	gap: 0.5rem;
	font-size: var( --text-base );
}

.picker-search input
{
	padding: 0.35rem 0.5rem;
	font: inherit;
	font-size: var( --text-md );
	min-width: 14rem;
}

/* The pager takes the far end of the row, so the count and the search stay together on the left
   however wide the view is. */
.picker-pages
{
	display: flex;
	align-items: center;
	gap: 0.5rem;
	margin-left: auto;
}

.picker-tools .details-description { margin: 0; }

/* The whole name is the label for its own checkbox, which is the difference between a 13px target
   and a 200px one -- and the reason the checkbox carries an aria-label as well is that the label
   element here is doing hit area, not naming. */
.person-label { cursor: pointer; }

/* The chip and the name are one thing, on one line. The picker's label lost this to the same
   `.details-form label { display: grid }` rule as the search caption above -- one column, so the chip
   took a row and the name took the next -- and the members table and the matrix now wrap the pair
   in .person-identity for the same shape. The address rides on either as a `title`. */
.details-form .person-label,
.person-identity
{
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
}

.details-form label { display: grid; gap: 0.25rem; font-size: var( --text-base ); }

.details-form input[ type="text" ],
.details-form input[ type="url" ],
.details-form input[ type="email" ],
.details-form input[ type="number" ],
.details-form textarea
{
	padding: 0.45rem 0.55rem;
	font: inherit;
	/* The description is prose and the title is a name; both are read at body size rather than at
	   the smaller size of their own label. The other textual types are named because naming only
	   `text` is a defect this stylesheet has now shipped three times: Email stood shorter than Name
	   (the .user-panel-fields patch below), and the Rightsholder link drew its address at --text-sm
	   beside a Rightsholder at --text-md (Jon, 2026-10-10 -- "one of the fields is not like the
	   others"). A field a person types words into reads at body size whatever its validation type. */
	font-size: var( --text-md );
}

/* A LOCAL PADDING MAY NOT OUT-GROW THE SYSTEM HEIGHT. The rule above is written for the textarea,
   where 0.45rem of vertical padding around 1rem prose is right; applied to the single-line title
   field it produced 40.4px against the token's 37.6, so the Title input stood 2.8px taller than the
   Save button beneath it and than every other field in the project. Measured, not guessed.

   The horizontal padding is unchanged -- it is the vertical that fights the token -- and the
   font-size stays at body size, which was the point of the rule above. `min-height` from the system
   rule then does the work, so this field is the same height as the others because it reads the same
   number and not because this padding was tuned to match. */
.details-form input[ type="text" ],
.details-form input[ type="url" ],
.details-form input[ type="email" ],
.details-form input[ type="number" ] { padding-block: 0.3rem; }

.details-form textarea { resize: vertical; min-height: 4.5rem; }

/* THE MARKDOWN CHEAT SHEET (index.php's renderMarkdownCheatSheet(), 2026-09-05). A <details>
   under the description textarea whose summary reads as a small link rather than as a heading with
   a triangle: it is an aside a person reaches for once, and a disclosure-sized control would make
   the form look like it had two sections. The default marker goes for the same reason, in both
   spellings the browsers use. The negative top margin pulls it up under the textarea so it reads as
   the field's own footnote rather than as the next thing in the form's grid. */
.markdown-help { margin-top: -0.4rem; font-size: var( --text-base ); }

.markdown-help > summary
{
	display: inline;
	cursor: pointer;
	color: var( --ink-quiet );
	text-decoration: underline;
	text-underline-offset: 0.15em;
	list-style: none;
}

.markdown-help > summary::-webkit-details-marker { display: none; }
.markdown-help > summary:hover, .markdown-help[open] > summary { color: var( --accent ); }

/* Two narrow columns, the syntax in the monospace the description itself uses for `code`, so what a
   person sees here is what they will type. No borders: eight rows of two words do not need ruling
   to be read, and a ruled table under a textarea reads as data rather than as help. */
/* `fit-content`: a sheet as wide as the textarea above it is a second panel; one as wide as its own
   two columns is a note. */
.markdown-help-sheet { width: fit-content; max-width: 100%; margin-top: 0.5rem; padding: 0.6rem 0.75rem; border: 1px solid var( --rule ); border-radius: 6px; }
.markdown-help-sheet table { border-collapse: collapse; }
.markdown-help-sheet th { text-align: left; font-weight: 600; color: var( --ink-quiet ); padding: 0 1.25rem 0.25rem 0; }
.markdown-help-sheet td { padding: 0.1rem 1.25rem 0.1rem 0; vertical-align: baseline; }
.markdown-help-sheet code { font-size: 0.92em; white-space: nowrap; }

/* PREVIEW, beside the cheat sheet (lib/markdownfield.php, D521). One row under the box: the sheet's
   summary and the Preview button read as two small links side by side, aligned on the first line,
   so the sheet, when open, grows down under its own summary while the button stays beside it.
   The button exists only once markdown-paste.js has loaded, which adds
   `markdown-preview-ready` to <html>: without script there is nothing for it to do. The panel is the
   shell's own `.scope-description`, so the preview is drawn in the type the page will use; here it
   only loses the negative margin that pulls a description under its heading, and gets more height. */
.markdown-tools { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 1.1rem; margin-top: -0.4rem; }
.markdown-tools > .markdown-help { margin-top: 0; }
/* Under a field that ends in its own help line (a presentation's description) there is no bare textarea
   edge to tuck up against, and the pull would lay the row over the help's last line. */
.details-field + .markdown-tools { margin-top: 0.15rem; }

.markdown-preview-toggle
{
	display: none;
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	font-size: var( --text-base );
	cursor: pointer;
	color: var( --ink-quiet );
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

.markdown-preview-ready .markdown-preview-toggle { display: inline; }
.markdown-preview-toggle:hover, .markdown-preview-toggle[aria-pressed="true"] { color: var( --accent ); }

.scope-description.markdown-preview
{
	max-width: 100%;
	max-height: 20rem;
	margin: 0.5rem 0 0.9rem;
	/* Set apart a little more than a description on its page (Jon): `--rule` alone all but vanishes on
	   a dialog's ground, so the edge is mixed from the ink, which every theme keeps legible. */
	padding: 0.75rem 1rem;
	border: 1px solid color-mix( in srgb, var( --ink-quiet ) 40%, transparent );
	border-radius: 0.5rem;
}
.markdown-help-sheet p { margin: 0.5rem 0 0; }

/* Aligned left with the fields rather than stretched across them: a save button as wide as a
   textarea reads as the primary action of the whole page, which it is not. */
/* Same reasoning as the key image controls: this was a bare <button>, so the browser drew it, and a
   platform-blue submit button is the one element on the page whose appearance nobody here chose. */
/* .details-save no longer says anything about SIZE. It carried `padding: 0.4rem 0.85rem` and
   `font-size: 0.85rem`, which is what made the item page's "Save" a different height from every
   other button on it; --control-height owns that now. The class stays because the markup names it
   and because it is the honest name for "the button that completes this panel" -- but it is a role,
   not a size. */
.details-save { }

/* Every pill in a details form is aligned left with the fields rather than stretched across them --
   a grid item's default `stretch` made the removal confirmation's "Remove <name>" button as wide as
   the panel, which reads as the primary action of the whole page rather than as the last step of
   one disclosure. It was on .details-save alone, so the one button that is NOT a save missed it. */
.details-form > .control-pill { justify-self: end; }

/* pre-line, not pre: newlines a person typed are meaningful, the wrapping they typed inside a
   fixed-width textarea is not. */
.details-description
{
	white-space: pre-line;
	max-width: 46rem;
	margin: 0;
}

/* ---------------------------------------------------------------------------------------------
   The key image panel: where the picture came from, and the controls that change it (D138).
   --------------------------------------------------------------------------------------------- */

/* The provenance sentence, for the states that still get one.
   
   "Auto-generated" and "Chosen frame" are badges beside the heading now; what is left down here is
   the case where a badge cannot carry the meaning -- a borrowed picture names the item it came from,
   and `pending` says why what you are looking at is not what you asked for. Those two are the only
   states where a sentence earns its line. */
.key-image-provenance
{
	margin: 0.5rem 0 0;
	font-size: var( --text-base );
	opacity: 0.85;
}

.key-image-provenance[ data-state="pending" ] { opacity: 1; font-style: italic; }
.key-image-provenance[ data-state="none" ] { opacity: 0.6; }

/* THE KEY IMAGE'S OWN CONTROLS ARE ACTION ROWS NOW, so the layout they used to need is gone: the
   frame row and the promotion row are `.action-row` and nothing else, and the five lines that used
   to explain that `.key-image-note` "is the button's LABEL, not a paragraph that happens to sit
   beside it" went with them. That explanation was the shape apologising for itself; a title line
   above a detail line needs no apology (ui-design.md §4.2).

   `display: none` and not merely `hidden`, because `.action-row` sets `display: flex` and a bare
   [hidden] attribute loses to it. The form is revealed by item.js only when there is a player to
   take a position from -- which keeps it out of the accessibility tree as well as off the screen
   for a reader with no JavaScript, rather than announcing a control that cannot work. */
.key-image-frame-form[ hidden ] { display: none; }

/* The undo of the frame row, and it stays a pill in the pill row above (§4.2's falsifier: "Back to
   the automatic frame" has no verb to strip down to). `display: contents` so the button itself is
   the flex item, and the form around it -- which exists only to carry a token -- does not become a
   box of its own inside a row of chips. Same reasoning as `.affordance-form` had before it. */
.key-image-reset { display: contents; }

/* ---------------------------------------------------------------------------------------------
   THE KEY IMAGE CROPPER (D365) -- a 16:9 frame dragged over the card's own thumbnail.

   It sits inside the action row's text column, under the sentence that explains it, because the row
   is the proposition and this is the part of it a person operates. `hidden` in the markup until
   item.js has measured the picture: without script there is no rectangle to submit, and a visible
   control that can only fail is worse than an absent one.

   The stage is `inline-block` and shrink-wraps the picture EXACTLY. That is not styling, it is what
   makes the arithmetic work: the frame is positioned in percentages of the stage, so the stage and
   the picture have to be the same box or every percentage is measured against the wrong thing.
   --------------------------------------------------------------------------------------------- */

.key-image-cropper
{
	margin: .7rem 0 0;
	max-width: 30rem;
}

.cropper-stage
{
	position: relative;
	display: inline-block;
	max-width: 100%;
	line-height: 0;
	border-radius: .25rem;
	overflow: hidden;
	background: var( --rule );
	touch-action: none;
}

.cropper-stage img
{
	display: block;
	max-width: 100%;
	height: auto;
	/* The picture outside the frame is dimmed rather than hidden, which is the whole readability of
	   this control: what is being given up has to stay visible, or the person is choosing a rectangle
	   without seeing what it excludes. */
	opacity: .45;
	user-select: none;
	-webkit-user-drag: none;
}

/* THE FRAME. A hole in the dimming, made with a shadow large enough to cover any picture rather than
   with a second copy of the image -- one element, no second download, and it stays exact at every
   size. */
.cropper-frame
{
	position: absolute;
	box-sizing: border-box;
	border: 1px solid var( --page-background );
	outline: 1px solid var( --accent );
	box-shadow: 0 0 0 100vmax color-mix( in srgb, var( --page-background ) 55%, transparent );
	cursor: move;
}

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

.cropper-handle
{
	position: absolute;
	width: .85rem;
	height: .85rem;
	background: var( --page-background );
	border: 1px solid var( --accent );
	border-radius: .15rem;
}

/* Each corner sits ON its corner, half outside the frame, so the hit target is the corner itself
   rather than a square just inside it. */
.cropper-handle[data-handle="nw"] { left: -.45rem; top: -.45rem; cursor: nwse-resize; }
.cropper-handle[data-handle="ne"] { right: -.45rem; top: -.45rem; cursor: nesw-resize; }
.cropper-handle[data-handle="sw"] { left: -.45rem; bottom: -.45rem; cursor: nesw-resize; }
.cropper-handle[data-handle="se"] { right: -.45rem; bottom: -.45rem; cursor: nwse-resize; }

/* The instruction, and beside it the one number that matters: how many of the picture's own pixels
   the chosen rectangle actually contains. A card preview is around 248 px wide and asks for twice
   that on a retina screen, so this is the honest way to show what a tight crop costs -- rather than
   letting somebody discover it later on a card. */
.cropper-hint
{
	margin: .4rem 0 0;
	font-size: var( --text-sm );
	line-height: 1.45;
	color: var( --ink-quiet );
}

/* A note rather than a warning: being generated in Buffet is a fact about an asset's origin, not a
   problem with it. Marked with a rule down the side so it reads as an annotation on the item rather
   than as another field of it. */
.derived-from
{
	margin: 1.5rem 0;
	padding: 0.6rem 0 0.6rem 0.9rem;
	border-left: 3px solid currentColor;
	font-size: var( --text-base );
	opacity: 0.85;
	max-width: 46rem;
}

.key-image-borrowers { margin: 1.5rem 0; }

.key-image-borrower-list
{
	list-style: none;
	margin: 0.75rem 0 0;
	padding: 0;
	display: grid;
	gap: 0.35rem;
}

.key-image-borrower-list li { display: flex; align-items: baseline; gap: 0.5rem; }


/* ---------------------------------------------------------------------------------------------
   The two states a person is waiting on, drawn on the card's picture (D183).

   WHY THEY LEFT THE INDICATOR STRIP. Everything in that strip is a standing property of the item --
   how many versions, whether it is a draft, who can see it -- rendered as quiet grey text at .715rem.
   "Uploading" and "Processing" are the only two entries that are about to change by themselves, and
   at that size and weight a grid that is half busy is indistinguishable from a grid that is idle.
   They keep their place in the strip; this is the same fact, promoted to where the eye already goes.

   Positioned rather than stacked because the preview slot is `display: grid; place-items: center`
   and holds either an image or the extension placeholder: an overlay must not push either of them.
   --------------------------------------------------------------------------------------------- */

.item-card .card-progress
{
	position: absolute;
	inset-inline: .4rem;
	bottom: .4rem;
	padding: .2rem .45rem;
	border-radius: .8rem;
	background: var( --accent );
	/* Not var( --ink ): this sits on the accent, which is dark green in the light theme and light
	   green in the dark one, so a token that follows the theme would be unreadable in one of them. */
	color: var( --card-background );
	font-size: var( --text-2xs );
	font-weight: 600;
	letter-spacing: .01em;
	text-align: center;
	/* The picture underneath stays legible: this is a status, not a lid. */
	box-shadow: 0 1px 4px color-mix( in srgb, var( --ink ) 25%, transparent );
}

/* Uploading is the one with no artefact behind it at all, so it can afford to be the louder of the
   two. Processing often sits over a poster that already arrived, and shouting over a picture the
   person is trying to look at is how a status turns into an obstruction. */
.item-card .card-progress[data-state="uploading"]
{
	animation: attention-pulse var( --attention-rhythm ) ease-in-out infinite;
}

.item-card[data-processing="working"] .card-progress
{
	opacity: .92;
}

@media ( prefers-reduced-motion: reduce )
{
	.item-card .card-progress[data-state="uploading"] { animation: none; }
}


/* The sentence under the extension: what kind of absence this is (D183). Quiet, because the
   extension above it is the identifying part and this is the explanation -- and deliberately not
   hidden behind a title attribute, since §8.4's "an absence must read as a supported outcome" is
   not served by a fact only a mouse can find. */
.no-preview-note
{
	display: block;
	margin-top: .35rem;
	max-width: 28ch;
	color: var( --ink-quiet );
	font-size: var( --text-2xs );
	line-height: 1.35;
	text-wrap: balance;
}


/* ---------------------------------------------------------------------------------------------
   FYI -- the mention control and the cards it produces (D265, app/lib/mentions.php).

   Two surfaces, one vocabulary. The CONTROL is a disclosure in a pill row, so it borrows
   .scope-panel-toggle / .control-pill / .scope-panel wholesale rather than growing a second
   disclosure mechanism -- so it opens as the same modal dialog in either toolbar (D493). The
   CARD is a dashboard rail, so it takes --card-track like every other grid on that page.
   --------------------------------------------------------------------------------------------- */

/* THE PICKER IS A TOOL AREA (Jon, 2026-09-05): the same box as the share panel on the item page and
   the surfaces above it, so the disclosure reads as a panel of the page rather than as a form that
   fell out of the toolbar. .tool-area carries its own 1.5rem vertical margin for the page flow; here
   the box is the whole dialog, so it drops it. */
.mention-panel > .tool-area { margin: 0; }

/* ---------------------------------------------------------------------------------------------
   THE PICKER (D427, app/lib/picker.php): one box for "choose some of these, for that set of things".

   These rules were `.mention-candidate*` until 2026-09-18, written for the FYI panel alone. The item
   menu's keyword picker needed the same box with two more facets -- a third state per row and a
   field that mints rows -- and Jon asked for the two to be one view. So the FYI panel now draws
   through picker.php and these are the only rules either has. Everything the old block learned is
   kept, and the comments say where it was learned.
   --------------------------------------------------------------------------------------------- */

/* The form takes the box's width. .details-form's 46rem is a measure for reading, and inside a box
   that runs the column's width it would stop the textarea and the submit two-thirds of the way
   across, with the border carrying on past them -- a button that is not at the edge of the thing it
   completes. */
.picker > .details-form { max-width: none; }

/* WHAT IT ACTS ON, said before anything can be ticked. Weighted like a value rather than a caption:
   it is the one line that stops a menu opened on one card from quietly acting on twelve. */
.picker-subject
{
	margin: 0 0 .15rem;
	font-size: var( --text-base );
	font-weight: 650;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.picker-note
{
	margin: 0 0 .5rem;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

.picker-entry-label { font-size: var( --text-base ); }

/* The options, as a wrapping grid of rows: three is one column, forty fills the width in columns
   of at least 16rem, which is a name plus a reason with room to spare. Wider than that and the
   checkbox is a long way from the name it belongs to. Scrolls INSIDE the box: forty rows tall would
   push the box's own submit button off the screen. */
.picker-options
{
	margin: 0;
	padding: 0;
	border: 0;
	min-width: 0;
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( 16rem, 1fr ) );
	gap: .1rem .75rem;
	max-height: 14rem;
	overflow-y: auto;
}

/* Nothing to show -- no rows at all, or none matching what was typed. The sentence is the server's,
   carried on the box (`data-picker-empty`), and itemmenu.js only says WHEN. */
.picker-options[data-empty="true"]::after
{
	content: attr( data-picker-empty );
	font-size: var( --text-base );
	color: var( --ink-quiet );
	padding: .3rem .4rem;
}

/* `.picker-form .picker-option`, two classes, and the second class is not decoration. The rule this
   label has to beat is `.details-form label` -- a class and an element, 0,1,1 -- and a bare
   `.picker-option` at 0,1,0 LOSES to it, which is exactly what the FYI picker's first version did:
   the inherited one-column grid stacked the checkbox above the name.

   Two columns: the checkbox, then the name with the note UNDER it. Beside the name, a sentence like
   "has not accepted their invitation yet" squeezed the name onto two lines to make room. */
.picker-form .picker-option
{
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: center;
	column-gap: .5rem;
	row-gap: .05rem;
	padding: .3rem .4rem;
	border-radius: .35rem;
	font-size: var( --text-base );
	cursor: pointer;
}

/* An author `display` beats the UA's `[hidden]`, so a filtered-out row owes its own rule (the
   selection bar learned this the hard way, further down this file). */
.picker-form .picker-option[hidden] { display: none; }

.picker-form .picker-option input[ type="checkbox" ]
{
	margin: 0;
	accent-color: var( --accent );
}

.picker-form .picker-option-note { grid-column: 2; }

.picker-option:hover { background: var( --selection-background ); }

/* A row the person has changed and not yet applied. The box writes only what changed, so what
   changed has to be findable at a glance in a list of sixty. */
.picker-option[data-changed="true"] .picker-option-name
{
	font-weight: 650;
}

/* The keyword picker's names wear the `#` the search box taught, as the chips do. */
.picker[data-picker="keywords"] .picker-option-name::before { content: "#"; opacity: .55; }

/* An option that is refused. Dimmed AND explained -- the dimming says "not available", the note
   under it says why, and ui-design.md §4's rule is that the reason is readable without a mouse. */
.picker-option[data-eligible="false"] { color: var( --ink-quiet ); }
.picker-option[data-eligible="false"]:hover { background: none; }
.picker-form .picker-option[data-eligible="false"] { cursor: default; }

.picker-option-note
{
	font-size: var( --text-sm );
	color: var( --ink-quiet );
	font-style: italic;
}

.picker-message-label textarea
{
	resize: vertical;
	min-height: 3.5rem;
}

.picker-empty
{
	margin: 0;
	font-size: var( --text-base );
	color: var( --ink-quiet );
}


/* ---------------------------------------------------------------------------------------------
   THE ITEM MENU (D427): the kebab on a card or a list row, and the menu it opens.
   --------------------------------------------------------------------------------------------- */

/* The kebab, at the right edge of the card body's top right corner with the star to its left
   (the star's own rule, in the pin block above, says why they are in the body and always shown). */
.item-card .item-menu-button
{
	position: absolute;
	grid-row: 2;
	top: .5rem;
	right: .3rem;
	z-index: 2;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	display: grid;
	place-items: center;
	border-radius: 50%;
	color: var( --ink-quiet );
	transition: color .12s ease, background .12s ease;
}

.item-card .item-menu-button .icon { width: var( --icon-button-glyph ); height: var( --icon-button-glyph ); }

.item-menu-button:hover,
.item-menu-button:focus-visible,
.item-menu-button[aria-expanded="true"]
{
	color: var( --ink );
	background: var( --selection-background );
	outline: none;
}

/* IN A ROW THE KEBAB BELONGS TO THE TITLE CELL, not to the row's right end where the star is. The
   list is wider than most windows and scrolls sideways, so the right end of a row is usually off
   screen -- and a menu whose whole purpose is reaching verbs FROM a row cannot live where the row
   has to be scrolled to find it (seen on a screen at 1400px, 2026-09-18).

   It cannot simply become a cell: the heading row and every item row share `--list-columns`, and
   one extra cell puts every column one out (the folder cell taught that). What it uses instead is
   a rule of CSS Grid that reads like a loophole and is not one: an ABSOLUTELY POSITIONED child of
   a grid container that names a grid area takes THAT AREA as its containing block, and because it
   is out of flow it occupies no cell and auto-placement never sees it. So `grid-column: 2` makes
   `right: 0` mean "the right edge of the title column", whatever width that column resolved to. */
[data-library-layout="list"] .item-card .item-menu-button
{
	grid-column: 2 / 3;
	grid-row: 1;
	top: 50%;
	right: 0;
	transform: translateY( -50% );
	background: var( --card-background );
	backdrop-filter: none;
}

/* ...and the title stops short of it, so a long one ends in its ellipsis rather than under a disc. */
[data-library-layout="list"] .item-card .card-title { padding-right: 1.7rem; }

/* The photo grid hides every word and the star with them, and has no body to put a button in. The
   kebab stays as the one way from a tile to a verb: over the picture, in the corner, on the disc
   a glyph over a photograph needs, and on hover -- a wall of pictures is the point of this layout. */
[data-library-layout="grid"] .item-card .item-menu-button
{
	grid-row: 1;
	right: .4rem;
	background: color-mix( in srgb, var( --card-background ) 78%, transparent );
	backdrop-filter: blur( 4px );
	opacity: 0;
}

[data-library-layout="grid"] .item-card:hover .item-menu-button,
[data-library-layout="grid"] .item-card[data-selected="true"] .item-menu-button,
[data-library-layout="grid"] .item-menu-button:focus-visible,
[data-library-layout="grid"] .item-menu-button[aria-expanded="true"] { opacity: 1; }

@media ( hover: none )
{
	[data-library-layout="grid"] .item-card .item-menu-button { opacity: 1; }
}

/* The menu. `position: fixed` and placed by itemmenu.js beside whatever opened it; above the dock
   and the toasts' neighbours, below nothing, because while it is open it is what is being done. */
.item-menu[hidden] { display: none; }

.item-menu
{
	position: fixed;
	z-index: 45;
	display: flex;
	flex-direction: column;
	width: 15rem;
	max-width: calc( 100vw - 1rem );
	max-height: min( 34rem, calc( 100vh - 1rem ) );
	border: 1px solid var( --rule );
	border-radius: .5rem;
	background: var( --card-background );
	box-shadow: 0 6px 24px rgba( 20, 22, 26, .18 ), var( --shadow-card );
	font-size: var( --text-base );
}

/* A panel is a form and wants a form's width. */
.item-menu[data-panel="keywords"],
.item-menu[data-panel="mention"] { width: 23rem; }

.item-menu-chrome
{
	flex: none;
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: .2rem .25rem 0;
}

/* The chrome holds only Back, which a panel needs and the bare menu does not. There is no close
   cross (Jon, 2026-09-24): an OS menu has none -- Escape or a click anywhere else closes it. */
.item-menu[data-panel="menu"] .item-menu-chrome,
.item-menu[data-panel="message"] .item-menu-chrome { display: none; }

.item-menu-chrome-button
{
	display: inline-flex;
	align-items: center;
	gap: .25rem;
	border: 0;
	border-radius: .35rem;
	background: none;
	color: var( --ink-quiet );
	font: inherit;
	font-size: var( --text-sm );
	padding: .3rem .4rem;
	cursor: pointer;
}

.item-menu-chrome-button[hidden] { display: none; }
.item-menu-chrome-button:hover { color: var( --ink ); background: var( --selection-background ); }
.item-menu-chrome-button .icon { width: .85rem; height: .85rem; }

.item-menu-content
{
	min-height: 0;
	overflow-y: auto;
	padding: .35rem;
}

/* It holds focus after a pointer opens the menu (itemMenuShow()) and is not itself a thing to press. */
.item-menu-content:focus { outline: none; }

.item-menu-body { display: flex; flex-direction: column; gap: .05rem; }

.item-menu-subject
{
	margin: 0;
	padding: .3rem .5rem .4rem;
	border-bottom: 1px solid var( --rule );
	margin-bottom: .25rem;
	font-size: var( --text-sm );
	font-weight: 650;
	color: var( --ink-quiet );
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.item-menu-form { margin: 0; display: contents; }

/* Archive or Remove, whichever Option is not choosing (D569) -- `display: contents` above beats the UA's [hidden]. */
.item-menu-form[hidden] { display: none; }

.item-menu-entry
{
	display: flex;
	align-items: center;
	gap: .55rem;
	width: 100%;
	border: 0;
	border-radius: .35rem;
	background: none;
	color: var( --ink );
	font: inherit;
	text-align: left;
	padding: .45rem .5rem;
	cursor: pointer;
}

.item-menu-entry:hover,
.item-menu-entry:focus-visible
{
	background: var( --selection-background );
	outline: none;
}

.item-menu-entry .icon { width: 1rem; height: 1rem; flex: none; color: var( --ink-quiet ); }
.item-menu-entry-label { flex: 1; min-width: 0; }

/* ---------------------------------------------------------------------------------------------
   A CONTAINER'S MENU IS ITS ACTION BAR (D604), drawn as a column of entries.

   The markup inside is the bar's own -- scopepanels.php's renderScopeActionBar(), called by shell/scopemenu.php's
   renderScopeMenu() -- so these rules do one job: make a row of pills read as a menu. Every pill
   that is an ENTRY loses its border and its capsule and takes the entry's shape; a pill INSIDE one
   of the bar's dialogs is left exactly as it is on the place's page, which is what `:not( dialog * )`
   is for throughout. Nothing here names a particular control, so a pill added to the bar is an
   entry the day it is added.
   --------------------------------------------------------------------------------------------- */
.item-menu .scope-actions-menu
{
	flex-direction: column;
	flex-wrap: nowrap;
	align-items: stretch;
	gap: .05rem;
	margin: 0;
}

/* A form that wraps one pill (the star, Archive) is not a box of its own in the column. */
.item-menu .scope-actions-menu form:not( dialog * ) { display: contents; margin: 0; }

.item-menu .scope-actions-menu .control-pill:not( dialog * )
{
	--icon-size: 1rem;

	display: flex;
	justify-content: flex-start;
	gap: .55rem;
	width: 100%;
	min-height: 0;
	border: 0;
	border-radius: .35rem;
	background: none;
	color: var( --ink );
	font-size: inherit;
	text-align: left;
	padding: .45rem .5rem;
}

.item-menu .scope-actions-menu .control-pill:not( dialog * ):hover,
.item-menu .scope-actions-menu .control-pill:not( dialog * ):focus-visible,
.item-menu .scope-actions-menu .scope-panel-toggle:focus-visible + .control-pill
{
	background: var( --selection-background );
	outline: none;
}

.item-menu .scope-actions-menu .control-pill:not( dialog * ) > .icon { color: var( --ink-quiet ); flex: none; }

/* Refused with its reason in the title, as on the bar: still listed, visibly not on offer. */
.item-menu .scope-actions-menu .control-pill:not( dialog * ):disabled,
.item-menu .scope-actions-menu .control-pill:not( dialog * )[aria-disabled="true"]
{
	opacity: .5;
	cursor: default;
	background: none;
}

/* The Inbox's sentence, under the entries rather than among them. */
.item-menu .scope-actions-menu > .notice,
.item-menu .scope-menu-body > .notice { margin: .25rem 0 0; white-space: normal; font-size: var( --text-xs ); }

/* WHILE ONE OF THE PLACE'S DIALOGS IS OPEN the menu is out of sight: the dialog is in the top layer
   and would otherwise stand over a dimmed menu that is no longer the thing being answered. It
   cannot be `hidden` -- the dialog is its descendant, and a hidden ancestor takes the dialog with
   it -- so it is made invisible and the dialog made visible again. itemmenu.js closes the menu when
   the dialog closes. */
.item-menu:has( dialog[open] ) { visibility: hidden; }
.item-menu dialog[open] { visibility: visible; }

/* The kebab on a container's tile: in the corner the star had, with the star moved in beside it.
   Shown as the star is -- on hover, on focus, while its menu is open -- and always where there is
   no hover to show it with. */
.scope-tile-item .scope-menu-button
{
	position: absolute;
	top: 50%;
	right: .35rem;
	transform: translateY( -50% );
	z-index: 2;
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	display: grid;
	place-items: center;
	border-radius: 50%;
	color: var( --ink-quiet );
	opacity: 0;
	transition: opacity .12s ease, color .12s ease, background .12s ease;
}

.scope-tile-item .scope-menu-button .icon { width: var( --icon-button-glyph ); height: var( --icon-button-glyph ); }

.scope-tile-item:hover .scope-menu-button,
.scope-tile-item .scope-menu-button:focus-visible,
.scope-tile-item .scope-menu-button[aria-expanded="true"] { opacity: 1; }

/* THE STAR AND THE KEBAB ARE A PAIR (Jon, on a screen, 2026-10-05). The star alone sat in the tile's
   top corner as a bare glyph; beside a kebab in a round box, centred a few pixels lower, the two read
   as misplaced rather than as neighbours. So on a tile that has a kebab both are the same box, on
   the tile's own middle line, side by side -- and the tile keeps that much room clear at its right
   end, so a long name ends before them instead of running underneath. */
.scope-tile-item:has( .scope-menu-button ) .pin-button
{
	top: 50%;
	right: calc( .35rem + var( --icon-button-size ) );
	transform: translateY( -50% );
	width: var( --icon-button-size );
	height: var( --icon-button-size );
	display: grid;
	place-items: center;
	padding: 0;
	border-radius: 50%;
}

.scope-tile-item:has( .scope-menu-button ) > .scope-tile { padding-right: calc( .35rem + var( --icon-button-size ) + .3rem ); }
.scope-tile-item:has( .scope-menu-button ):has( .pin-button ) > .scope-tile { padding-right: calc( .35rem + var( --icon-button-size ) * 2 + .3rem ); }

/* The icon layout draws a tile as a glyph over a name and already drops the star; the kebab goes
   with it, and the right-click is the menu there. */
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .scope-menu-button { display: none; }

@media ( hover: none )
{
	.scope-tile-item .scope-menu-button { opacity: 1; }
}

.item-menu-entry-note
{
	flex: none;
	font-size: var( --text-xs );
	color: var( --ink-quiet );
}

.item-menu-message,
.item-menu-empty
{
	margin: 0;
	padding: .5rem 2rem .5rem .5rem;
	font-size: var( --text-base );
	color: var( --ink-quiet );
}

/* Copy link's fallback (D484): when the clipboard refused, the address itself, selected, for the
   keyboard to copy. Wrapped anywhere, because an address is one unbroken word wider than the menu,
   and a one-row field that scrolled sideways hid it behind its own scrollbar (seen on a screen). */
.item-menu-copy-field
{
	display: block;
	box-sizing: border-box;
	width: calc( 100% - 1rem );
	margin: 0 .5rem .5rem;
	font-size: var( --text-sm );
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	overflow-y: auto;
	resize: none;
}

/* A picker inside the menu: the tool area's box is the menu's own box here, so it sheds its border
   and margin, and its options run in one column because the menu is one column wide. `:not( dialog * )`
   for the same reason as the menu rules above: a container's menu (D604) carries the bar's own
   <dialog> panels along in its markup, and inside one of those the tool area IS the dialog's visible
   box -- stripped, the Your color panel drew its swatches straight onto the dimmed page (seen on a
   screen, 2026-10-05). A dialog's tool area keeps its box wherever the dialog's markup lives. */
.item-menu .tool-area:not( dialog * )
{
	margin: 0;
	padding: .1rem .4rem .4rem;
	border: 0;
	background: none;
	box-shadow: none;
}

.item-menu .tool-area-header:not( dialog * ) { margin-bottom: .4rem; }
.item-menu .tool-area-summary:not( dialog * ) { font-size: var( --text-sm ); }
.item-menu .picker-options:not( dialog * ) { grid-template-columns: 1fr; max-height: 13rem; }

/* A phone: a sheet along the bottom edge, where a thumb is, rather than a popover placed against a
   control the keyboard is about to cover. `!important` because itemmenu.js writes `left` and `top`
   inline for the wide case and an inline style outranks everything else. */
@media ( max-width: 40rem )
{
	.item-menu,
	.item-menu[data-panel="keywords"],
	.item-menu[data-panel="mention"]
	{
		left: 0 !important;
		right: 0;
		top: auto !important;
		bottom: 0;
		width: auto;
		max-width: none;
		max-height: 80vh;
		border-radius: .75rem .75rem 0 0;
		border-bottom: 0;
		padding-bottom: env( safe-area-inset-bottom );
	}

	.item-menu-entry { padding: .7rem .6rem; }
}


/* --- The rail. Cards, because a message is the point: the activity rails below this one are tiles
   saying where and when, and a sentence somebody wrote needs a line of its own to be read rather
   than a cell to be scanned. */
.fyi-cards
{
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( var( --card-track, 15.5rem ), 1fr ) );
	gap: var( --card-gap, .7rem );
	margin: 0;
	padding: 0;
	list-style: none;
}

.fyi-card
{
	display: flex;
	flex-direction: column;
	gap: .45rem;
	padding: .7rem .8rem;
	border: 1px solid var( --rule );
	border-radius: .55rem;
	background: var( --card-background );
	/* The one card on the dashboard that is ADDRESSED to the person reading, so it is the one card
	   allowed an accent edge. Everything else on this page is something that happened; this is
	   something somebody asked of you. */
	border-inline-start: 3px solid var( --accent );
}

.fyi-from
{
	margin: 0;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

/* The message, at body size: it is the content of the card, and setting it at the caption size of
   the line above would make the metadata louder than the thing itself. `pre-wrap` is not used --
   the message is escaped and run through nl2br, so its line breaks are already markup. */
.fyi-message
{
	margin: 0;
	font-size: var( --text-base );
	line-height: 1.4;
	overflow-wrap: anywhere;
}

.fyi-target
{
	display: flex;
	align-items: center;
	gap: .4rem;
	margin-top: auto;
	font-size: var( --text-base );
	font-weight: 600;
	color: var( --ink );
}

.fyi-target:hover { text-decoration: underline; }

.fyi-target .icon { --icon-size: .95rem; flex: none; }

.fyi-target-name { overflow-wrap: anywhere; }

/* The dismiss control sits at the foot of its own card rather than as a cross in the corner: it says
   what it does, which is the rule for anything that looks destructive. It is not destructive --
   dismissal is a state and the row survives -- but the person pressing it does not know that. */
.fyi-dismiss { margin: 0; }
.fyi-dismiss .control-pill { font-size: var( --text-xs ); padding: .2rem .5rem; }

.fyi-overflow
{
	margin: .6rem 0 0;
	font-size: var( --text-sm );
	color: var( --ink-quiet );
}

.fyi-dismiss-all { margin: .6rem 0 0; }


/* ---------------------------------------------------------------------------------------------
   MOVING THINGS: the drag vocabulary, the selection, and the undo offer.

   D268 (order), D273 (the move), ui-design.md U8/U10/U28/F15. mover.js owns the behaviour; this is
   the half a person actually sees, and the whole design rests on one sentence from D273:

       A REORDER IS PRESENTATION AND A MOVE IS A DISCLOSURE, and they must not share a gesture
       without a visible difference.

   So the two gestures look nothing alike while a drag is in flight, and the difference is carried by
   the DROP TARGET rather than by a modifier key nobody can see:

       [data-dragging="true"]               the thing in flight, faded -- every carried thing, not
                                            only the one under the pointer
       [data-drop-state="before"/"after"]   a thin caret BETWEEN two things -- a reorder
       [data-drop-state="into"]             the container FILLS -- a move (or, on Favorites, a pin)
       [data-drop-state="refused"]          a warning outline -- nothing will happen, and it says so
       [data-drop-received="true"]          one flash on what caught the drop -- it landed
       [data-spring="armed"]               a closed branch under a held drag, about to open

   The third is the one this project keeps having to relearn: a target that looks armed and is not is
   ui-design.md's recurring complaint, and U28 is the same complaint about the uploader's drop zone.
   A refused target is drawn refused rather than drawn normally and then silently doing nothing.

   **ONE VOCABULARY FOR BOTH ENGINES** (D300). sidebar.js's drag -- reorder a row, drop a row on
   Favorites -- speaks these attributes too. It used to have its own (`data-drop`), its own fade rule
   and a DASHED "into" on Favorites, which in this block's language is a refusal; so the same gesture
   looked like two features, and the one place a person could pin by dragging looked like the one
   place they could not. The sidebar keeps exactly one rule of its own, the axis of its caret.
   --------------------------------------------------------------------------------------------- */

/* The thing in flight. A generic rule, because a card, a tile and a sidebar row are the same
   gesture and a drag that faded one and did something else to another would read as two features. */
[data-dragging="true"]
{
	opacity: .4;
}

/* THE CARD IS THE DRAG SOURCE, WHATEVER IS UNDER THE POINTER. A card wraps an image and two links,
   and each of those is a drag source in its own right: the browser started the drag from whichever
   was grabbed, so a thumbnail dragged a picture and a title dragged a URL, and setDragImage() in a
   handler two elements up did not always win. `-webkit-user-drag` is the WebKit/Blink property for
   exactly this; index.php sets `draggable="false"` on the same elements for engines that read the
   attribute. And no text selection inside a draggable card: macOS was starting a SELECTION drag
   from the words in a thumbnail, which is a third kind of ghost and a third kind of payload. */
.item-card[draggable="true"]
{
	-webkit-user-drag: element;
	-webkit-user-select: none;
	user-select: none;
}

/* A LONG PRESS IS A DRAG ON A TOUCH SCREEN, AND THE BROWSER'S OWN ANSWER TO A LONG PRESS IS IN
   THE WAY. iPadOS starts a native drag from a `draggable` element on touch-and-hold -- but a
   sidebar row is a link and a card wraps one, and Safari's answer to a held link is the preview
   callout, which is the "default contextual menu" Jon met sorting the navigation on an iPad
   (2026-09-16). `-webkit-touch-callout` is the WebKit property for exactly that, on every drag
   source and on the link and picture inside a card, because the callout belongs to the innermost
   element under the finger. Android's equivalent is a `contextmenu` event, which mover.js declines
   for the same elements. No `touch-action: none` anywhere here: the grid is made of cards and the
   page has to scroll from them, so the hold is what starts the drag and a swipe still scrolls.
   Tiles and rows gain the no-selection rule the card and the handle already had, since a held
   finger on a tile's name otherwise begins a text selection instead of a drag. */
.item-card[draggable="true"],
.item-card[draggable="true"] a,
.item-card[draggable="true"] img,
.scope-tile[draggable="true"],
.title-handle[draggable="true"],
.sidebar-row[draggable="true"],
.favorite-row
{
	-webkit-touch-callout: none;
}

.scope-tile[draggable="true"],
.sidebar-row[draggable="true"],
.favorite-row
{
	-webkit-user-select: none;
	user-select: none;
}

.item-card[draggable="true"] a,
.item-card[draggable="true"] img
{
	-webkit-user-drag: none;
}

/* THE TITLE AS A HANDLE -- "drag this" (index.php's renderTitleHandle()). The heading of a folder's
   page and an item's page is the thing itself, picked up by its name: the family's or the scope's
   glyph at the start, and, where the person may move it, a drag that mover.js carries exactly as it
   carries a tile or a card.

   At rest it is a heading and nothing else -- the glyph in the quiet ink and the words where they
   always were. The negative leading margin is what keeps them there: the padding exists only so the
   hover chip has room around the words, and taking it back out means the resting title sits flush
   with the page's left edge as it did before it was a handle. Only the LEADING side: taken back on
   the trailing side too, the lifted chip ran into the kind badge beside a folder's heading. **No `max-width: 100%`**, which it
   had for one screenshot: in a flex heading the <h1> is sized to the handle's MARGIN box, 0.76em
   narrower than its border box, so a cap of 100% took exactly the padding back off the words and
   wrapped a two-word folder name onto two lines. Shrink-to-fit already bounds an inline-flex box by
   the space it has, which is the only limit it needs.

   On hover, and only when the drag is offered, it LIFTS: a chip in the accent's quiet fill, a
   hairline ring, the card's own shadow, the glyph turning accent, and the grab cursor. Every one of
   those is borrowed from something that is already a thing you can pick up in this interface --
   the fill and ring from the "into" mark, the shadow from a card -- so it reads as an object rather
   than as a link, which is the difference that matters: a link goes somewhere when pressed, this
   goes somewhere when carried. The tooltip says so in words. And because the browser photographs
   the drag image from the element as it looks at dragstart, the ghost that follows the pointer IS
   the lifted chip -- the proxy the person grabbed, not a URL or a screenshot of text.

   No text selection inside it, for the card's reason above: a drag begun on the words would
   otherwise start a selection drag, which is a third ghost and a third payload. The title is still
   copyable from the rename control and from the address it carries into a text field. */
.title-handle
{
	display: inline-flex;
	align-items: center;
	gap: .38em;
	margin: -.12em 0 -.12em -.38em;
	padding: .12em .38em;
	border-radius: .5rem;
	transition: background-color .12s ease, box-shadow .12s ease, transform .12s ease;
}

.title-handle-glyph
{
	/* .78em until 2026-10-02, when the glyph moved after the name (renderTitleHandle()) and Jon asked
	   for it a little larger: trailing the title it reads as the kind's tag, and at .78 it was a speck. */
	--icon-size: .92em;
	color: var( --ink-quiet );
	transition: color .12s ease;
}

.title-handle-text
{
	min-width: 0;
	overflow-wrap: anywhere;
}

.title-handle[draggable="true"]
{
	cursor: grab;
	-webkit-user-drag: element;
	-webkit-user-select: none;
	user-select: none;
}

.title-handle[draggable="true"]:hover
{
	background: var( --accent-quiet );
	box-shadow: inset 0 0 0 1px color-mix( in srgb, var( --accent ) 40%, transparent ), var( --shadow-card );
}

.title-handle[draggable="true"]:hover > .title-handle-glyph
{
	color: var( --accent );
}

.title-handle[draggable="true"]:active
{
	cursor: grabbing;
}

@media ( prefers-reduced-motion: no-preference )
{
	.title-handle[draggable="true"]:hover
	{
		transform: translateY( -1px );
	}
}

/* A move target, filling. `outline` rather than `border` deliberately: a border would resize the
   element and shove every neighbour a pixel sideways at the exact moment somebody is aiming at it. */
[data-drop-state="into"]
{
	outline: 2px solid var( --accent );
	outline-offset: -2px;
	background: var( --accent-quiet );
}

/* A target this actor may not write to. Rendered from the server's own answer
   (`data-drop-allowed`), never guessed at in the browser -- and drawn at all, rather than being
   inert, because a drop that vanishes is indistinguishable from a broken feature. */
[data-drop-state="refused"]
{
	outline: 2px dashed var( --warning );
	outline-offset: -2px;
	background: var( --warning-quiet );
	cursor: not-allowed;
}

/* A CLOSED BRANCH ABOUT TO SPRING OPEN (sidebar.js's spring loader). The hold has been noticed and
   the open is coming: the accent ring, blinking twice in the 600 ms before it opens (sidebar.js's
   SIDEBAR_SPRING_* pair, slowed at D606 -- two blinks of .3s fill exactly that wait), on the ROW of a
   collapsed branch or the HEADING of a collapsed section -- never on the <li> or the <section>, whose
   box would ring the hidden children's space as well. Under reduced motion the ring simply holds. */
li[data-spring="armed"] > .sidebar-row,
.sidebar-section[data-spring="armed"] > .sidebar-section-heading,
.app-footer .status-tray-toggle[data-spring="armed"],
.shelf-handle[data-spring="armed"]
{
	outline: 2px solid var( --accent );
	outline-offset: -2px;
	border-radius: .35rem;
}

@media ( prefers-reduced-motion: no-preference )
{
	li[data-spring="armed"] > .sidebar-row,
	.sidebar-section[data-spring="armed"] > .sidebar-section-heading,
	.app-footer .status-tray-toggle[data-spring="armed"],
	.shelf-handle[data-spring="armed"]
	{
		animation: spring-armed .3s steps( 1, end ) 2;
	}
}

@keyframes spring-armed
{
	0%   { outline-color: var( --accent ); }
	50%  { outline-color: transparent; }
	100% { outline-color: var( --accent ); }
}

/* THE PAGE ITSELF, ARMED FOR A FILE (D366). The whole main region is a drop target now, not just
   the invitation inside it, so the two rules above land on an element the size of the window.

   Three adjustments, and each is about scale rather than taste. The outline is drawn just inside the
   scroll padding rather than at -2px, because a ring hard against the edge of a scrollport reads as
   a browser focus ring on the document. The fill is the same `--accent-quiet` everything else uses,
   which at this size is the difference between "the window will take this" and a window that has
   turned blue. And `outline` survives the scroll: .app-main is the scroll container, so the ring is
   painted on its border box and stays around the visible region wherever the content is scrolled to,
   which is exactly what a page-level arm has to do and what a border on the content would not.

   The specific target -- a tile, a sidebar row -- wears the same `into` fill and wins by being the
   only thing marked: upload.js marks one element at a time, so crossing from open page onto a tile
   moves the mark rather than adding a second one. */
.app-main[data-drop-state]
{
	outline-offset: -.5rem;
}

/* An item card is not a target in its own right (Jon, D366) and it is not a hole either: the drop
   falls through to the place the page is. So nothing here paints a card during a file drag -- the
   region's own fill runs under it, which is the whole visual statement of "anywhere on this page".
   Cards keep their move marks, which come from mover.js and a different gesture entirely. */

/* The insertion slot for a card reorder: ONE ELEMENT, drawn in the gap BETWEEN two cards, placed
   by mover.js from the target card's rectangle (moverShowSlot()). It used to be an inset shadow on
   the neighbouring card's edge, which read, in Jon's words, as "a curvy border-colour on the
   adjacent card" -- the slot a thing will go into has to look like a slot, not like a property of
   the card beside it. A card clips its own overflow, so nothing drawn on the card can reach the
   gap; hence a sibling in the grid, positioned absolutely so it takes no track, and removed with the
   marks. Vertical between cards that flow across; horizontal between rows in the list layout and
   between rows in the sidebar, where sidebar.js places it inside the scroll region. */
.mover-slot
{
	position: absolute;
	z-index: 2;
	margin: 0;
	padding: 0;
	list-style: none;
	pointer-events: none;
	border-radius: 2px;
	background: var( --selection-accent );
}

/* The flash that says a drop landed. It is the "into" look -- ring and fill -- shown once and taken
   away, so what a target promised while the pointer was over it is what it confirms after.

   Drawn with an inset box-shadow rather than outline and background, for two reasons. The ring must
   not fight the marks that use outline and persist -- a selection ring, the "changed while you were
   away" mark -- and the fill must animate to NOTHING: a background animated to transparent would,
   for the frame between the animation's end and the attribute's removal, take a card's own background
   with it. A transparent shadow is simply no shadow. The large spread is the fill; inset, it paints
   over the background and under the content, like the "into" fill does.

   The colour is the information, so it holds for the whole 900 ms (mover.js) and survives reduced
   motion; the fade is the only movement, and it is the one thing that goes. Same duration as the
   uploader's `dropzone-received` above, which this generalises. */
[data-drop-received="true"]
{
	box-shadow: inset 0 0 0 2px var( --accent ), inset 0 0 0 100vmax var( --accent-quiet );
}

@media ( prefers-reduced-motion: no-preference )
{
	[data-drop-received="true"]
	{
		animation: drop-received .9s ease-out 1 forwards;
	}
}

@keyframes drop-received
{
	0%   { box-shadow: inset 0 0 0 2px var( --accent ), inset 0 0 0 100vmax var( --accent-quiet ); }
	40%  { box-shadow: inset 0 0 0 2px var( --accent ), inset 0 0 0 100vmax var( --accent-quiet ); }
	100% { box-shadow: inset 0 0 0 2px transparent, inset 0 0 0 100vmax transparent; }
}

/* THE CONFIRMATION, HOSTED. A move that widens an audience is confirmed by the server's own
   interstitial (D273 rule 4), and that page used to be navigated to: the whole shell went away,
   the sidebar came back reset, and confirming reloaded again. mover.js now fetches it and hosts its
   `.interstitial` panel in a <dialog> over the shell -- the same markup, the same sentence, the same
   form, decided and rendered by the server; only the window it appears in is the shell's. The
   dialog is a frame and nothing else, so the panel's own rules above still draw it. */
.mover-dialog
{
	max-width: min( 36rem, calc( 100vw - 2rem ) );
	padding: 0;
	border: 0;
	background: transparent;
	color: inherit;
}

.mover-dialog::backdrop
{
	background: color-mix( in srgb, var( --ink ) 38%, transparent );
}

.mover-dialog .interstitial
{
	margin: 0;
	box-shadow: var( --shadow-card );
}

/* A landed notice, lifted into the toast host (mover.js's moverHoistLandedNotices()). The notice's
   own children -- glyph, sentence, the undo form -- go into the toast body as they are; the notice
   wrapper, with its banner background and its bottom margin, stays behind. */
.toast-body .notice-glyph
{
	margin-right: .35rem;
}

.toast-body .move-undo-form
{
	display: inline-block;
	margin: .35rem 0 0;
}

/* The composed ghost for a multi-card drag (mover.js's moverSetDragImage()): a clone of the grabbed
   card under a count. Off-screen rather than hidden, because setDragImage() wants an element that
   is rendered, and removed on the next tick once the browser has taken its picture. */
.mover-ghost
{
	position: fixed;
	top: -100vh;
	left: 0;
	pointer-events: none;
}

.mover-ghost .item-card
{
	list-style: none;
	opacity: 1;
}

.mover-ghost-count
{
	position: absolute;
	top: -.5rem;
	right: -.5rem;
	min-width: 1.6rem;
	padding: 0 .4rem;
	border-radius: 1rem;
	background: var( --accent );
	color: var( --page-background );
	font-size: var( --text-sm );
	font-weight: 600;
	line-height: 1.6rem;
	text-align: center;
}

/* A selected card. A ring rather than a wash, so the picture underneath is still readable -- the
   whole point of selecting twenty photographs is to look at them while you decide. */
.item-card[data-selected="true"],
.scope-tile[data-selected="true"]
{
	outline: 2px solid var( --selection-accent );
	outline-offset: -2px;
}

/* The bar's one teaching sentence, present only while the selection is a single item. Quiet, and
   the first thing to go when the bar is short of room: the count and the buttons are the bar. */
.selection-hint
{
	flex: 0 1 auto;
	min-width: 0;
	overflow: hidden;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
	text-overflow: ellipsis;
	white-space: nowrap;
}

.selection-hint[hidden]
{
	display: none;
}

/* The selection bar. Hidden until something is selected, and `hidden` is toggled rather than a
   display rule, so it is absent from the accessibility tree too and not merely invisible.

   The `[hidden]` rule below is what makes that true, and its absence is what broke it. The user-agent
   stylesheet's `[hidden] { display: none }` loses to ANY author declaration of `display`, whatever the
   selector's specificity, so `display: flex` here silently re-showed a bar mover.js believed it had
   hidden -- and the bar sat on every page, on arrival and on every reload, with nothing selected. An
   author rule that sets `display` on an element that is ever `hidden` owes a matching `[hidden]` rule. */
.selection-bar[hidden]
{
	display: none;
}

/* IT HOVERS, AND TAKES NO ROOM (Jon, 2026-09-21: *"The selection toolbar nudges the layout down
   needlessly. It can hover over page content"*). One click selects since D458, so the bar now
   arrives on the first click anybody makes in a listing -- and a bar in the flow pushed every card
   down by its own height at exactly the moment the pointer was resting on one of them, which is
   ui-design.md §1.2 broken by the shell itself. The toast host's rule, stated there: an announcement
   must never move the thing it is announcing.

   A ZERO-HEIGHT STICKY STRIP with the pill hanging out of it, rather than `position: fixed`: sticky
   measures from .app-main, which is the scroll container, so the pill stays in view down a long
   listing and stays inside the content column whatever the sidebar's width or state is, with no
   arithmetic about either. `pointer-events` as for the toasts: the strip spans the column and must
   not eat a click meant for the card under its empty ends.

   AT THE BOTTOM, and the top was tried first: there it sat on the view's own heading, which is the
   one line of a page that says where you are. The pill hangs UPWARD out of the strip.

   `top`, NOT `bottom`, AND THE STRIP STAYS FIRST IN THE REGION. `position: sticky; bottom` only
   holds an element that comes AFTER the content, and moving the bar to the end of the region was
   tried: it broke the promise renderMainRegion() makes to ops/task9 -- the camera floater is the
   region's last line, alone -- and eight assertions with it. A sticky `top` pushes an element DOWN
   until it is that far from the top of the scrollport, and a percentage there is a percentage of
   the scrollport's height, so this is "one bar's height above the bottom edge" said from the other
   end, with the markup left exactly where every reader expects it. On a view shorter than the
   window the pill stops at the end of the content, which is where the eye is. */
.selection-bar
{
	position: sticky;
	top: calc( 100% - 1.2rem );
	z-index: 30;
	display: flex;
	justify-content: center;
	align-items: flex-end;
	height: 0;
	margin: 0;
	overflow: visible;
	pointer-events: none;
}

.selection-bar-pill
{
	pointer-events: auto;
	flex: 0 1 auto;
	display: flex;
	gap: .6rem;
	align-items: center;
	max-width: 100%;
	padding: .45rem .7rem;
	border: 1px solid var( --rule );
	border-radius: 2rem;
	/* Opaque under the tint: it sits over pictures now, and --accent-quiet alone is translucent in
	   some themes and let a thumbnail read through the count. */
	background: linear-gradient( var( --accent-quiet ), var( --accent-quiet ) ), var( --card-background );
	box-shadow: 0 .35rem 1.1rem rgb( 0 0 0 / .22 );
	font-size: var( --text-base );
}

/* The undo offer. It looks like a toast and it is NOT one: it is server-rendered markup in the page
   the move landed on (shell/scopepanels.php's renderMoveUndoOffer), because §19.2 executes no client JavaScript
   and an undo for the one write that changes an audience is the wrong half to leave unassertable.
   mover.js only fades it; with no script at all it simply stays until the next navigation, which is
   the safe direction to be wrong in. */
.move-undo
{
	display: flex;
	gap: .75rem;
	align-items: center;
	transition: opacity .6s ease;
}

.move-undo[data-fading="true"]
{
	opacity: 0;
}

.move-undo-form
{
	margin: 0 0 0 auto;
}

/* The interstitial (D273 rule 4). A page of its own, with nothing else on it: it is the one moment
   this interface asks somebody to think before continuing, and a dialog competing with a sidebar,
   a toolbar and a grid of photographs is a dialog nobody reads. */
.interstitial-body
{
	margin: 0;
	background: var( --page-background );
	color: var( --ink );
	font-family: var( --font-ui, system-ui, sans-serif );
}

.interstitial
{
	max-width: 34rem;
	margin: 3rem auto;
	padding: 1.5rem;
	border: 1px solid var( --rule );
	border-radius: 6px;
	background: var( --card-background );
}

.interstitial h1
{
	margin-top: 0;
	font-size: var( --text-lg );
}

.interstitial-detail
{
	line-height: 1.5;
}

/* What KEEPS working (rule 3), quieter than the warning above it. A dialog that lists only what
   changes invites the reader to conclude the opposite of the truth -- that moving a shared folder
   revokes its own link -- so this sentence is present, and subordinate. */
.interstitial-reassurance
{
	color: var( --ink-quiet );
	font-size: var( --text-base );
}

/* THE BUTTON DOCK (ui-design.md §4.3), and this panel is the second surface to adopt it.

   `flex-end`, and the markup order is Cancel then "Move it anyway" -- the default sits rightmost
   and source order is visual order is tab order, so no `order` property and no `row-reverse`. The
   two buttons were the other way round until 2026-09-04, and the default was drawn in whatever the
   browser draws: `.button-primary` and `.button-secondary` were the only two class names in this
   project defined by NO stylesheet at all. That is D258's lesson twice over -- a class can be fully
   styled and do nothing, and it can also be fully named and never styled -- so both are gone and
   the panel wears .control-pill like every other pair of buttons here. */
.interstitial-form
{
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	gap: .75rem;
	align-items: center;
	margin-top: 1.25rem;
}


/* ---------------------------------------------------------------------------------------------
   The person page (D329). The heading wears the same chip the tiles and tables use, scaled to the
   h1 it sits in; the tables' `cursor: help` is for a chip that carries an email in its title, and
   this one carries nothing, so the cursor is put back.
   --------------------------------------------------------------------------------------------- */

[data-person-page] h1
{
	/* Flex rather than inline-grid on a text baseline: the chip is a box with no baseline of its own,
	   and WebKit and Gecko placed it a few pixels apart from Blink under `vertical-align`. Centring
	   the two boxes on each other asks nothing of the baseline and lines up in all three. */
	display: inline-flex;
	align-items: center;
	gap: .5rem;
}

[data-person-page] h1 .person-chip
{
	width: 2rem;
	height: 2rem;
	flex: none;
	font-size: var( --text-sm );
	cursor: default;
}

.person-link
{
	color: inherit;
	text-decoration: none;
}

.person-link:hover .person-chip,
.person-link:focus-visible .person-chip
{
	outline: 2px solid var( --accent );
	outline-offset: 1px;
}

.person-link:hover
{
	text-decoration: underline;
}

/* ---------------------------------------------------------------------------------------------
   Folder actions (D344): the gallery, an action's panel, the log.

   A card is a description with at most one control on it -- Configure jumps to the panel, and the
   switch is the one verb that belongs beside a name. The state badge reuses .badge and colours
   only ON, because "off" and "not configured" are the resting states and a coloured resting state
   is a badge that competes with a control (D135's inversion).
   --------------------------------------------------------------------------------------------- */

.actions-section-heading { margin: 1.8rem 0 .6rem; font-size: var( --text-lg ); letter-spacing: -.01em; }

.action-gallery
{
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( 18rem, 1fr ) );
	gap: .8rem;
	max-width: 60rem;
}

.action-card
{
	display: grid;
	gap: .5rem;
	padding: .9rem 1rem;
	border: 1px solid var( --rule );
	border-radius: 8px;
	background: var( --card-background );
}

.action-card header { display: grid; grid-template-columns: minmax( 0, 1fr ) auto; align-items: center; gap: .4rem .6rem; }
.action-card h3 { margin: 0; font-size: var( --text-md ); }
.action-card-badges { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: .4rem; }
.action-card-badges:empty { display: none; }
.action-card p { margin: 0; font-size: var( --text-base ); line-height: 1.45; color: var( --ink-quiet ); }
.action-card .badge[data-status="planned"] { background: color-mix( in srgb, var( --ink-quiet ) 12%, transparent ); }
.action-card .badge[data-state="on"] { background: color-mix( in srgb, var( --create ) 18%, transparent ); color: var( --create ); }
.action-card .badge[data-status="built"] { background: color-mix( in srgb, var( --accent ) 14%, transparent ); color: var( --accent ); }
.action-card-controls { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.action-card-controls form { display: inline; margin: 0; }
/* A descriptor's tile_note -- the gallery's View as guest -- sits at the right end of the footer. */
.action-card-controls .action-card-note { margin-left: auto; display: inline-flex; align-items: center; }
.action-card-controls a.badge { text-decoration: none; color: inherit; }

/* THE SWITCH (D488): one control on a tile and at the top right of a pane, Active in green and
   Inactive in red, from the theme's own create and danger tones so every theme keeps the meaning.
   The word beside the track is the STATE, never a verb. A disabled one (not configured, or not
   yours to change) keeps its colour at half strength, so the state still reads. */
.action-toggle-form { display: inline-flex; margin: 0 0 0 auto; }
.action-card header > .action-toggle-form,
.action-card header > .action-toggle { grid-column: 2; grid-row: 1; justify-self: end; margin: 0; }
/* margin: 0 above, at this specificity, because the switch's form also wears `.scope-actions` and a
   bar margin given to that layout (experiments.css's 1.8rem, 2026-10-07) made row 1 taller and pushed
   the title down on every card whose switch is live -- the dimmed one is a <span> and stayed put. */
.folder-action-panel .tool-area-header > .action-toggle { margin-left: auto; }
.action-toggle { display: inline-flex; align-items: center; gap: .45rem; padding: .15rem .2rem; border: 0; background: none; color: var( --ink ); font: inherit; font-size: var( --text-sm ); cursor: pointer; white-space: nowrap; }
.action-toggle-track { position: relative; flex: none; width: 2.1rem; height: 1.2rem; border-radius: 999px; background: var( --danger ); transition: background-color .15s ease; }
.action-toggle-knob { position: absolute; top: .15rem; left: .15rem; width: .9rem; height: .9rem; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba( 0, 0, 0, .25 ); transition: transform .15s ease; }
.action-toggle[data-active="yes"] .action-toggle-track { background: var( --create ); }
.action-toggle[data-active="yes"] .action-toggle-knob { transform: translateX( .9rem ); }
.action-toggle[data-active="yes"] .action-toggle-word { color: var( --create ); }
.action-toggle[data-active="no"] .action-toggle-word { color: var( --danger ); }
.action-toggle[aria-disabled="true"] { cursor: default; opacity: .5; }
button.action-toggle:hover .action-toggle-track { filter: brightness( 1.08 ); }
button.action-toggle:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; border-radius: 999px; }
@media ( prefers-reduced-motion: reduce ) { .action-toggle-track, .action-toggle-knob { transition: none; } }

/* The pane's foot (D488): Cancel, Save configuration, Save and activate -- right-aligned, the primary last. */
.action-panel-dock { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .45rem; margin-top: .9rem; }

/* THE PANE IS A MODAL DIALOG (D489): in the middle of the window, over a backdrop that blocks the
   page, one at a time -- where it used to be a disclosure beneath the tiles that opened below the
   fold and queued. The dialog itself has no padding, so a click that reaches it is a click on the
   backdrop (app.js closes on that); the pane inside scrolls when it is taller than the window, and
   its foot of buttons stays at the bottom of what is read. */
.action-panel-dialog { width: min( 44rem, calc( 100vw - 2rem ) ); max-height: calc( 100dvh - 3rem ); padding: 0; border: 0; border-radius: 10px; background: transparent; color: var( --ink ); overflow: visible; }
/* A literal, not a token: ::backdrop inherits nothing in the Chrome this host runs, so a var() there is
   invalid at computed time and leaves the backdrop transparent -- which is what the first screenshot showed. */
.action-panel-dialog::backdrop { background: rgba( 16, 18, 20, .45 ); }
.action-panel-dialog .folder-action-panel { margin: 0; max-height: calc( 100dvh - 3rem ); overflow-y: auto; box-sizing: border-box; overscroll-behavior: contain; }
.action-panel-dock .control-pill[data-action-dialog-close]:not( :only-child ) { margin-right: auto; }
.gallery-link .share-url-hint { flex-basis: 100%; margin: -.15rem 0 0; font-size: var( --text-xs ); color: var( --ink-quiet ); }
.gallery-link .share-url-hint[ data-copied="true" ] { color: var( --create ); }
.action-panels { position: relative; }
.action-gallery-planned { margin-bottom: .8rem; }
.folder-action-panel .tool-area-header { display: flex; align-items: center; gap: .5rem; }
.folder-action-panel h3 { margin: 1.2rem 0 .4rem; font-size: var( --text-md ); }
/* `.details-form label` lays a label out as a grid (field under its words); a checkbox wants its
   words BESIDE it, so the two switches name the form to outrank that rule. */
.details-form label.folder-action-switch,
.details-form label.folder-action-ceiling { display: flex; align-items: center; gap: .45rem; font-size: var( --text-base ); }
.details-form label.folder-action-switch { margin: .2rem 0 .6rem; }
.folder-action-counts { margin: .2rem 0 .8rem; padding-left: 1.3rem; font-size: var( --text-md ); }
.folder-action-counts li { margin: 0 0 .2rem; }
.folder-action-conflicts { display: grid; gap: .7rem; padding: .7rem .9rem; border: 1px solid var( --rule ); border-radius: 6px; }
.folder-action-conflicts legend { font-size: var( --text-base ); color: var( --ink-quiet ); padding: 0 .3rem; }
.folder-action-conflict { display: grid; gap: .25rem; font-size: var( --text-base ); }
.folder-action-conflict ul { margin: 0; padding-left: 1.2rem; color: var( --ink-quiet ); }
.folder-action-conflict label { display: flex; align-items: center; gap: .4rem; }
.details-form label.folder-action-ceiling { color: var( --danger ); }

/* The Gallery's panel (D487): each fieldset is one wrapped row of choices, words beside their box,
   the legend a quiet heading above them -- the folder-action switch's shape, so the panel reads as
   three short rows rather than a column of eleven labels (Jon, 2026-09-23). The selector names the
   form and the fieldset to outrank `.details-form label`'s grid. */
.details-form fieldset.gallery-choice { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem 1.1rem; margin: .3rem 0 .7rem; padding: 0; border: 0; min-width: 0; }
.details-form fieldset.gallery-choice legend { float: left; width: 100%; margin: 0 0 .3rem; padding: 0; font-size: var( --text-sm ); color: var( --ink-quiet ); }
.details-form fieldset.gallery-choice > label { display: flex; align-items: center; gap: .4rem; font-size: var( --text-base ); white-space: nowrap; }
.details-form fieldset.gallery-choice > p { flex-basis: 100%; margin: .1rem 0 0; font-size: var( --text-sm ); }

/* Remove personal data's boxes (D496): one per row, the box beside its words and the hint under the
   words, so five choices with a sentence each read as a list rather than as ten stacked lines. The
   same outranking selector as the Gallery's, for the same `.details-form label` grid. */
.details-form fieldset.folder-action-categories { display: grid; gap: .45rem; margin: .3rem 0 .7rem; padding: 0; border: 0; min-width: 0; }
.details-form fieldset.folder-action-categories legend { margin: 0 0 .3rem; padding: 0; font-size: var( --text-sm ); color: var( --ink-quiet ); }
.details-form fieldset.folder-action-categories > label { display: grid; grid-template-columns: auto 1fr; column-gap: .5rem; row-gap: .05rem; align-items: center; font-size: var( --text-base ); }
.details-form fieldset.folder-action-categories > label > .quiet { grid-column: 2; font-size: var( --text-sm ); }
/* The Gallery pane's groups (Jon, 2026-09-23): Visibility and Preview are offer rows under an h3, the
   item page's shape; Layout is the form's choices under one. The links list is part of Visibility, so
   its heading is an h4 set quieter than the group's own. */
.gallery-pane-group { margin: 0 0 .6rem; }
.gallery-pane-group > .action-row { padding-top: .2rem; }

/* THE CALLOUT (Jon, 2026-09-25): a note set into a pane -- a bordered, quietly tinted box around content
   a person should read before the fields, first used for the Podcast's Checklist. It is a container,
   not a control: square-ish and flat, never a pill (ui-design §4.5's round-is-pressable rule). The
   tint is `--accent-quiet`, which every theme defines for both schemes, and the edge is mixed from the
   ink so it stays visible on a dialog's ground, where `--rule` all but disappears. Any element may
   wear it; its first heading sits flush at the top. */
.callout
{
	margin: 0 0 1rem;
	padding: .75rem 1rem .6rem;
	border: 1px solid color-mix( in srgb, var( --ink-quiet ) 35%, transparent );
	border-radius: .6rem;
	background: var( --accent-quiet );
}

.callout > :first-child { margin-top: 0; }
.callout > :last-child { margin-bottom: 0; }

/* The Release pill (D523) sits in line with what it releases: beside a Held badge, after an episode's
   name in the Checklist, in the share panel's row. Its form is only a carrier and takes no room. */
.item-release-form { display: inline; margin: 0; }
.podcast-checklist .podcast-private-episodes li { margin: .15rem 0; }
.podcast-checklist .podcast-private-episodes .control-pill { margin-left: .4rem; padding-block: .05rem; font-size: var( --text-sm ); }
.folder-action-panel h4.gallery-links-heading { margin: .7rem 0 .35rem; font-size: var( --text-sm ); font-weight: 600; color: var( --ink-quiet ); }
.folder-action-panel .action-question-warning { margin: .2rem 0 .4rem; color: var( --danger ); }
/* The Podcast pane's Checklist (Jon, 2026-09-25): a green check in a disc for what is good to go, the
   warning glyph for what still needs doing, and one line above them counting the two. `--create` is
   the green because it is the palette's one green that is not the accent, and it holds in both
   themes; `--warning` because a missing field is "be careful", not "this deletes". */
.podcast-checklist-summary { margin: 0 0 .4rem; font-size: var( --text-sm ); font-weight: 600; color: var( --warning ); }
.podcast-checklist-summary[data-ok="yes"] { color: var( --create ); }
.podcast-checklist > .action-row.podcast-ready,
.import-vimeo-connection > .action-row.import-vimeo-state { display: flex; align-items: flex-start; gap: .6rem; }
.podcast-checklist > .action-row.podcast-ready > .action-row-text,
.import-vimeo-connection > .action-row.import-vimeo-state > .action-row-text { flex: 1; min-width: 0; }
.podcast-checklist > .action-row.podcast-ready > .control-pill { flex: none; align-self: center; }
.podcast-check-mark,
.import-vimeo-mark { flex: none; display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; margin-top: .1rem; border-radius: 50%; }
.podcast-check-mark .icon,
.import-vimeo-mark .icon { width: 1rem; height: 1rem; }
.podcast-ready[data-ok="yes"] .podcast-check-mark,
.import-vimeo-state[data-ok="yes"] .import-vimeo-mark { color: var( --create ); background: var( --create-quiet ); }
.podcast-ready[data-ok="yes"] .podcast-check-mark .icon { stroke-width: 2.2; }
.podcast-ready[data-ok="no"] .podcast-check-mark,
.import-vimeo-state[data-ok="no"] .import-vimeo-mark { color: var( --warning ); background: var( --warning-quiet ); }
.podcast-ready[data-ok="no"] .action-row-title { color: var( --warning ); }
.podcast-private-episodes { margin: .3rem 0 0; padding-left: 1.1rem; font-size: var( --text-sm ); }
/* The Vimeo importer's pane (D653). Only the field the chosen source needs is shown -- the link for a
   folder or a showcase, the list for chosen videos, and "make Vimeo's folders into folders here" only
   where there are folders to make -- with `:has()`, so the pane runs no script. Every field is still
   posted; the handler reads only the one that applies. */
.import-vimeo-settings:not(:has(input[name="source_kind"][value="folder"]:checked)):not(:has(input[name="source_kind"][value="showcase"]:checked)) [data-source-field="reference"],
.import-vimeo-settings:not(:has(input[name="source_kind"][value="videos"]:checked)) [data-source-field="videos"],
.import-vimeo-settings:has(input[name="source_kind"][value="showcase"]:checked) [data-source-field="tree"],
.import-vimeo-settings:has(input[name="source_kind"][value="videos"]:checked) [data-source-field="tree"] { display: none; }
/* A machine's captions ride on captions: the second box is indented under the first, and greyed while
   the first is off (the handler drops it then too). */
.import-vimeo-carry [data-carry="automatic_captions"] { margin-left: 1.6rem; }
.import-vimeo-carry:has(input[name="carry_captions"]:not(:checked)) [data-carry="automatic_captions"] { opacity: .5; }
.import-vimeo-token { margin-top: .5rem; }
/* The token's two forms sit in the account box; Remove is a quiet second line under Save (D654). */
.import-vimeo-token-remove,
.import-vimeo-token-check { display: inline-block; margin: .2rem .4rem 0 0; }
.import-vimeo-check-facts { margin: .35rem 0 0; padding-left: 1.1rem; font-size: var( --text-sm ); }
.import-vimeo-check-facts [data-tone="warning"] { color: var( --warning ); }
.import-vimeo-preview-state { margin: .3rem 0; font-size: var( --text-sm ); }
.import-vimeo-preview-facts { margin: .2rem 0 .5rem; padding-left: 1.1rem; font-size: var( --text-sm ); }
.import-vimeo-preview-facts [data-tone="warning"] { color: var( --warning ); }
/* The tile's note (D657): the preview's state, beside Configure, while the pane is closed. */
.import-vimeo-tile-note { font-size: var( --text-sm ); color: var( --ink-quiet ); }
.import-vimeo-tile-note[data-tone="warning"] { color: var( --warning ); }
/* The cycle means nothing until watching is ticked: greyed, not hidden, so the choice is visible. */
.import-vimeo-watch:has(input[name="watch"]:not(:checked)) [data-watch-field="cycle"] { opacity: .5; }
.import-vimeo-panel .action-row-controls { display: flex; flex-wrap: wrap; gap: .4rem; margin: .3rem 0; }
/* The strip a gallery draws above its contents (D487). It had no rule of its own, and `.icon` is a
   block, so the glyph sat on its own line box and the sentence and its links hung off the baseline
   beside it. A wrapping row instead: the glyph centred on the first line, the sentence taking the
   room it needs, and Gallery settings and View as guest kept together after it. */
.curated-order-strip { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .6rem; margin: 0 0 .7rem; }
.curated-order-strip > .icon { align-self: center; }
.curated-order-strip > span { flex: 0 1 auto; min-width: 0; }
.curated-order-strip > a { white-space: nowrap; }
.gallery-links { list-style: none; margin: .2rem 0 .8rem; padding: 0; display: grid; gap: .4rem; }
.gallery-link { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.gallery-link-label { font-size: var( --text-base ); }
.gallery-link .share-url { flex: 1 1 18rem; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var( --text-sm ); padding: .35rem .5rem; text-overflow: ellipsis; cursor: pointer; }

/* The log sits in the item page's In depth box (item.css), closed, and scrolls inside it rather than
   growing the page (Jon, 2026-09-23). The header row stays put while the rows scroll under it. */
/* `details.` because item.css, linked after this sheet, sets .item-depth's own margin at equal weight. */
details.folder-action-log-area { margin: 1.75rem 0 1.5rem; max-width: 62rem; }
.folder-action-log-area .folder-action-log-body { max-height: 26rem; overflow: auto; padding: 0 1rem .5rem; }
.folder-action-log-area .folder-action-log thead th { position: sticky; top: 0; background: var( --card-background ); z-index: 1; }

.folder-action-log { border-collapse: collapse; font-size: var( --text-base ); max-width: 60rem; width: 100%; }
.folder-action-log th, .folder-action-log td { text-align: left; padding: .35rem .6rem; border-bottom: 1px solid var( --rule ); vertical-align: top; }
.folder-action-log th { font-weight: 600; color: var( --ink-quiet ); font-size: var( --text-sm ); letter-spacing: .04em; text-transform: uppercase; }
.folder-action-log .badge[data-tone="create"] { color: var( --create ); background: color-mix( in srgb, var( --create ) 16%, transparent ); }
.folder-action-log .badge[data-tone="accent"] { color: var( --accent ); background: color-mix( in srgb, var( --accent ) 14%, transparent ); }
.folder-action-log .badge[data-tone="danger"] { color: var( --danger ); background: color-mix( in srgb, var( --danger ) 14%, transparent ); }

/* The count on the Folder actions pill (D344): a badge inside a pill, after its words. */
.control-pill .pill-count { margin-left: .35rem; }

/* ...and the pill itself tinted while any action is on (Jon, 2026-09-23): the number says how
   many, the colour says at a glance that this folder does something to what lands in it.
   background-color, not background, so experiments.css's gradient image still lies over it. */
.control-pill.in-use { background-color: var( --accent-quiet ); }

/* ...and a dot when an automation that applies here was changed by somebody else since this person
   last opened the page (D486). A dot and not a word: the pill already has a word and a number, and
   the sentence is in its title and its aria-label. */
.control-pill .pill-changed
{
	display: inline-block; width: .55rem; height: .55rem; margin-left: .35rem; border-radius: 50%;
	background: var( --warning ); box-shadow: 0 0 0 2px var( --warning-quiet );
}

/* The tile's own marks (D486): where its state comes from, and that it changed. */
.action-card[data-changed] { border-color: var( --warning ); }
.action-card .badge[data-changed-badge] { background: var( --warning-quiet ); color: var( --warning ); }
.action-card .badge[data-origin-badge] { background: color-mix( in srgb, var( --ink-quiet ) 10%, transparent ); }

/* What a panel says about the tree (D486): where its answer comes from, and who beneath went their
   own way -- and the guard, which is the same list with the two choices under it. */
.folder-action-origin { margin: .3rem 0 .6rem; font-size: var( --text-base ); }
.folder-action-beneath { margin: .2rem 0 .8rem; font-size: var( --text-base ); }
.folder-action-beneath p { margin: 0 0 .3rem; }
.folder-action-beneath-list { margin: 0 0 .4rem; padding-left: 1.3rem; }
.folder-action-beneath-list li { margin: 0 0 .15rem; }
.folder-action-guard { display: grid; gap: .45rem; padding: .7rem .9rem; margin: .4rem 0 .8rem; border: 1px solid var( --warning ); border-radius: 6px; }
.folder-action-guard legend { font-size: var( --text-base ); padding: 0 .3rem; }
.details-form .folder-action-guard label { display: flex; align-items: center; gap: .45rem; font-size: var( --text-base ); }
.folder-action-guard p { margin: 0; }


/* ---------------------------------------------------------------------------------------------
   KEYWORDS (D348). Two renderings of one set of words, and neither is a .badge.

   `.keyword-list` is the read side: the words a thing carries, drawn as links to the keyword view
   under the description on an item and on a container, and as plain text on a guest document. A
   chip here is NAVIGATION, so it keeps the outline and the hover that app.css's badge rule reserves
   for the pressable -- the rule was "round is pressable, square is stated", and these are pressed.

   `.keyword-editor` is the write side, and it exists only when assets/keywords.js has run: the
   comma-separated text field is hidden and stands behind a box of badges each carrying a remove
   cross, with a bare text input at the end for the next word. The box borrows the field's own
   border so the two are one control to the eye, and the inner input has no border of its own.
   With the script absent the field is a field, and nothing below applies.
   --------------------------------------------------------------------------------------------- */

.keyword-list
{
	display: flex;
	flex-wrap: wrap;
	gap: .35rem;
	margin: .5rem 0 0;
	padding: 0;
	list-style: none;
}

.scope-keywords { margin: 0 0 1rem; }

.keyword-chip
{
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	padding: .14rem .55rem;
	border: 1px solid var( --rule );
	border-radius: 999px;
	font-size: var( --text-base );
	line-height: 1.3;
	color: inherit;
	text-decoration: none;
	/* Plainly transparent since 2026-10-10: this read `var( --surface, transparent )`, a token no
	   stylesheet defines, which is a fallback wearing a costume. A chip sits on whatever ground its
	   surface has; the editor's chips now sit on the field ground the editor itself draws. */
	background: transparent;
}

a.keyword-chip:hover { border-color: var( --ink-quiet ); }
a.keyword-chip:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }

.keyword-chip::before { content: "#"; opacity: .55; }

.keyword-editor
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .35rem;
	/* .4rem inline, so the entry input's own .15rem puts its text at .55rem from the edge -- the
	   same inset as every other field's text, which is what makes the box read as one of them. */
	padding: .3rem .4rem;
	border: 1px solid var( --rule );
	/* The field vocabulary, stated with the field's own values (2026-10-10, Jon: "Keywords would
	   need some attention, too"). This box stood at `border-radius: 4px` against every field's
	   .35rem, no minimum height, and `background: var( --field, transparent )` -- a token defined
	   nowhere in the project, so the one control dressed as a field had no field ground and sat as
	   a hole beside its neighbours in the dark palette. */
	border-radius: .35rem;
	background: var( --card-background );
	min-height: var( --control-height );
	cursor: text;
}

.keyword-editor:focus-within { outline: 2px solid var( --accent ); outline-offset: 1px; }

.keyword-editor .keyword-chip { padding-right: .2rem; }

.keyword-editor .keyword-remove
{
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* Four fifths of the small round button: it lives inside a chip, and a chip is a line of text. */
	width: calc( var( --icon-button-size ) * .8 );
	height: calc( var( --icon-button-size ) * .8 );
	margin-left: .1rem;
	padding: 0;
	border: 0;
	border-radius: 999px;
	font: inherit;
	font-size: var( --icon-button-glyph );
	line-height: 1;
	color: var( --ink-quiet );
	background: transparent;
	cursor: pointer;
}

.keyword-editor .keyword-remove:hover,
.keyword-editor .keyword-remove:focus-visible { color: inherit; background: var( --accent-quiet ); outline: none; }

/* `input.keyword-entry`, not `.keyword-entry`, and the difference is the whole rule: the entry is
   an input[type="text"], so `.details-form input[ type="text" ]` (one class, one attribute, one
   element) out-specified the old two-class selector and dressed the INNER input as a field -- its
   own 37.6px minimum and 8.8px side padding inside the box that is already the field. That is why
   the Keywords box stood 49px tall with its prompt indented past every other field's (measured
   2026-10-10; Jon: "Keywords would need some attention, too"). The element in the selector makes
   it a tie, and this rule is later in the file, so the entry is finally a bare line of text. */
.keyword-editor input.keyword-entry
{
	flex: 1 1 8rem;
	min-width: 6rem;
	min-height: 0;
	margin: 0;
	/* No block padding: the editor's own .3rem is the field's air, and any on the entry stacks on
	   top of it, standing the empty box proud of the 37.6px every other field shares. */
	padding: 0 .15rem;
	border: 0;
	font: inherit;
	font-size: var( --text-md );
	background: transparent;
}

.keyword-editor .keyword-entry:focus { outline: none; }

.details-form .keyword-editor + input[ data-keywords-field ] { display: none; }


/* ---------------------------------------------------------------------------------------------
   THE SAME LIST AGAIN, WITH A FLOOR UNDER IT ON A TOUCH SCREEN (Jon, 2026-09-18).

   iOS Safari zooms the whole page in when the field it is about to focus computes under 16px, and it
   does NOT zoom back out when that field is blurred. The person is left magnified and panned having
   asked for nothing -- and on the shell there is no way back, because `body.app-shell-body` is
   `overflow: hidden`: a fixed grid has no document scroll to pan with, so the half of the interface
   that has just moved off the screen is simply unreachable until they think to pinch. That pairing is
   what makes this read as "it randomly zooms in and gets stuck" rather than as a font being small,
   and it is why the report was about zooming and not about type size.

   Every text-entry control in the project was under the threshold, measured: `body` is 14px and these
   inherit it, and the ones that state a size of their own state a smaller one. The sign-in email
   field is the clearest case -- it is the first control anybody touches on a phone, and tapping it
   zooms the page before they have typed a character.

   THE FIX IS THE SIZE, NOT THE VIEWPORT, and that is not a preference. `user-scalable=no` and
   `maximum-scale=1` are the reflex here and they are unavailable to us: Blueprint §17 has
   accessibility as regulation rather than polish -- the European Accessibility Act and the Web
   Accessibility Directive both bind this service -- and WCAG 1.4.4 requires that a person be able to
   magnify deliberately. Removing the TRIGGER leaves the pinch alone, which is the outcome actually
   wanted: no zoom nobody asked for, and every zoom somebody did. Safari has ignored `user-scalable=no`
   for pinch since iOS 10 in any case, so the reflex would have spent the compliance and still not
   fixed the complaint.

   `max( 16px, 1em )` rather than a flat `16px`, so this is a floor and never a resize: a control that
   already computes larger keeps its own size. In a `font-size`, `1em` resolves against the parent --
   which is exactly the value a control wearing `font: inherit` would have taken anyway.

   `pointer: coarse` ALONE, and not the `max-width: 46rem` sheet breakpoint this file usually pairs it
   with: the question here is what is doing the touching, not how wide the window is. A narrow window
   on a desktop never auto-zooms, and a tablet wide enough to keep the sidebar column still does.
   IT LIVES AT THE END OF THE FILE, and that is load-bearing rather than tidy. The first draft of this
   sat next to the base rule above, where it reads better and where it does nothing: `.sidebar-search
   input` is declared at line ~740, `.toolbar-controls select` at ~1530 and `.picker-search input` at
   ~6960, all at the same specificity as the overrides here, and **the later rule wins a tie**. The
   block parsed, the media query matched, and every field stayed 14px. It was found by measuring the
   computed size in a real browser, which is the only place that particular mistake is visible.

   --------------------------------------------------------------------------------------------- */

@media ( pointer: coarse )
{
	input[ type="text" ],
	input[ type="search" ],
	input[ type="email" ],
	input[ type="url" ],
	input[ type="tel" ],
	input[ type="number" ],
	input[ type="password" ],
	input[ type="date" ],
	input[ type="datetime-local" ],
	select,
	textarea
	{
		font-size: max( 16px, 1em );
	}

	/* AND THE ONES THAT OUT-SPECIFY IT, NAMED. The rule above is a list of bare type selectors, so --
	   exactly as the base rule's comment says about backgrounds -- any control that states its own
	   size wins over it, INCLUDING the ones that state it only as `font: inherit`, which is a
	   declaration like any other and not an absence of one. That is most of the fields in the
	   project, so the floor without this half would have been a floor in name.

	   There is no `!important` anywhere in this stylesheet and this is not the place to introduce the
	   first one. Naming the components costs a line each and leaves the cascade legible; a bare
	   `!important` would have hidden the fact that eight rules in three files disagree with a floor
	   they never knew about. **If you give a field a font-size, give it a line here too** -- and if
	   it lives in item.css, share.css or signin.css, it needs the line in THAT file instead, because
	   all three load after this one and win every tie. */
	.sidebar-search input,
	.sidebar-standin select,
	.toolbar-controls select,
	.picker-search input,
	.grid-controls .grid-slideshow input[ type="number" ],
	.details-form input[ type="text" ],
	.details-form input[ type="url" ],
	.details-form input[ type="email" ],
	.details-form input[ type="number" ],
	.keyword-editor input.keyword-entry
	{
		font-size: max( 16px, 1em );
	}
}


/* =============================================================================================
   THE TEAM LEGEND (D433, lib/legend.php): a team's colours, emoji and state ladder for keywords.

   EIGHT TOKENS, NOT EIGHT COLOURS. `team_legend.color_token` stores a name, and what the name looks
   like is decided here -- once for light, once for dark -- so a legend survives a theme and a
   scheme, and a theme that wants its own red restates `--legend-red` and nothing else.

   THE TOKEN NEVER COLOURS TEXT. It paints a dot and a card's edge stripe; the word beside the dot
   is drawn in the ink it would have had anyway. That is what lets eight hues be chosen for being
   told apart rather than for passing 4.5:1 against every surface of every theme, and it is also
   the accessibility rule in one line: the colour is never the only thing saying it (WCAG 1.4.1).
   ============================================================================================= */

:root
{
	--legend-red: #c9372c;
	--legend-orange: #c8640c;
	--legend-yellow: #b08a00;
	--legend-green: #2e8540;
	--legend-teal: #0f8289;
	--legend-blue: #2a6fdb;
	--legend-purple: #7b4fc4;
	--legend-grey: #737a86;
}

@media ( prefers-color-scheme: dark )
{
	:root
	{
		--legend-red: #f07167;
		--legend-orange: #f09a4a;
		--legend-yellow: #e2c044;
		--legend-green: #5cc27a;
		--legend-teal: #4cc3c9;
		--legend-blue: #6ea8ff;
		--legend-purple: #b592f0;
		--legend-grey: #9aa1ac;
	}
}

[data-legend-color="red"], [data-legend-state-color="red"] { --legend-color: var( --legend-red ); }
[data-legend-color="orange"], [data-legend-state-color="orange"] { --legend-color: var( --legend-orange ); }
[data-legend-color="yellow"], [data-legend-state-color="yellow"] { --legend-color: var( --legend-yellow ); }
[data-legend-color="green"], [data-legend-state-color="green"] { --legend-color: var( --legend-green ); }
[data-legend-color="teal"], [data-legend-state-color="teal"] { --legend-color: var( --legend-teal ); }
[data-legend-color="blue"], [data-legend-state-color="blue"] { --legend-color: var( --legend-blue ); }
[data-legend-color="purple"], [data-legend-state-color="purple"] { --legend-color: var( --legend-purple ); }
[data-legend-color="grey"], [data-legend-state-color="grey"] { --legend-color: var( --legend-grey ); }

.legend-chip
{
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	max-width: 100%;
	font-size: var( --text-xs );
	line-height: 1.3;
	color: var( --ink-quiet );
	white-space: nowrap;
}

.legend-dot
{
	flex: none;
	width: .6rem;
	height: .6rem;
	border-radius: 50%;
	background: var( --legend-color, transparent );
}

/* A word the legend gives an emoji and no colour has no dot to draw, and an empty ring beside an
   emoji reads as a missing picture. */
.legend-chip:not( [data-legend-color] ) .legend-dot { display: none; }

.legend-word { overflow: hidden; text-overflow: ellipsis; }

/* THE STATE IS A TINTED BADGE AND A WORD IS A DOT, so the one exclusive thing on a card is told
   from the several that are not at a glance: tinted and ringed in its own colour, inked as ordinary
   text. A badge and not a pill (Jon, 2026-09-23): it states a fact, it is not a control, and its
   vertical padding is the card footer indicators' so the two share a height. */
.legend-chip.legend-state
{
	padding: .18rem .5rem .18rem .4rem;
	border-radius: 3px;
	color: var( --ink );
	background: color-mix( in srgb, var( --legend-color, var( --ink-quiet ) ) 16%, transparent );
	box-shadow: inset 0 0 0 1px color-mix( in srgb, var( --legend-color, var( --ink-quiet ) ) 45%, transparent );
}

.card-legend
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .25rem .55rem;
	margin: .3rem 0 0;
	min-width: 0;
}

.legend-more { font-size: var( --text-2xs ); color: var( --ink-quiet ); }

/* In the card footer a keyword and the "+n" are badges too, on the indicators' quiet ground, so the
   row is one kind of thing at one height. The state keeps its own tint and ring above, which is
   what still tells it from a word. Scoped away from the list, where the chips are a column's bare
   text. */
body:not( [data-library-layout="list"] ) .card-footer .legend-chip:not( .legend-state ),
body:not( [data-library-layout="list"] ) .card-footer .legend-more
{
	padding: .18rem .5rem .18rem .4rem;
	border-radius: 3px;
	background: color-mix( in srgb, var( --ink-quiet ) 9%, transparent );
	color: var( --ink );
	font-size: var( --text-xs );
	line-height: 1.3;
	white-space: nowrap;
}

body:not( [data-library-layout="list"] ) .card-footer .legend-more { padding-left: .5rem; color: var( --ink-quiet ); }

/* The edge stripe: the state's colour down the card's leading edge, the whole height of it. A
   pseudo-element and not a border, because the border is already spoken for three times over --
   selection, the focus ring and a drop target all draw on it -- and not an inset shadow either,
   which was the first attempt: a shadow is painted UNDER the card's children, so the preview
   covered it and the stripe began half way down (seen on a screen, 2026-09-19). Absolutely placed,
   so a card gaining a state does not move a pixel of the grid around it, and `pointer-events: none`
   so it never takes a click meant for the picture. */
.item-card[data-legend-state-color]::before,
.scope-tile[data-legend-state-color]::before
{
	content: "";
	position: absolute;
	z-index: 1;
	top: 0;
	bottom: 0;
	left: 0;
	width: .28rem;
	border-radius: .6rem 0 0 .6rem;
	background: var( --legend-color );
	pointer-events: none;
}

.scope-tile[data-legend-state-color] { position: relative; }

/* The list says the state in a column of its own and the grid draws no words at all. The stripe
   stays in all three: in the grid it is the only trace of a state there is room for. */
[data-library-layout="list"] .item-card .card-legend,
[data-library-layout="grid"] .item-card .card-legend
{
	display: none;
}

[data-library-layout="list"] .item-card[data-legend-state-color]::before
{
	width: .22rem;
	border-radius: 0;
}

[data-library-layout="list"] .meta-legend-state { min-width: 0; overflow: hidden; }

@media ( max-width: 62rem )
{
	[data-library-layout="list"] .meta-legend-state,
	[data-library-layout="list"] .head-state
	{
		display: none;
	}
}


/* The ladder as a control: one button per rung and one for none, in the item menu, on the item
   page. Each rung is the card's own state badge (renderLegendChip with `legend-state`), so the ring
   hugs the badge with a hair of room and the pill's tint is the rung's; "none" is a bare word. The
   pressed rung is ringed in ink -- not in its own colour, which a grey rung would lose. */
.ladder-rungs
{
	display: flex;
	flex-wrap: wrap;
	gap: .35rem;
}

.ladder-rung-form { margin: 0; }

.ladder-rung
{
	padding: .12rem .18rem;
	border: 1px solid transparent;
	border-radius: 5px;
	background: none;
	font: inherit;
	cursor: pointer;
}

.ladder-rung:hover { border-color: var( --rule ); }
.ladder-rung[aria-pressed="true"] { border-color: var( --ink ); }
.ladder-rung:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }

.item-menu-state
{
	padding: .45rem .7rem .55rem;
	border-top: 1px solid var( --rule );
}

.item-menu-state-label
{
	margin: 0 0 .35rem;
	font-size: var( --text-2xs );
	color: var( --ink-quiet );
}


/* The label over the row of marks, with the pencil that opens the palette in Settings (D442). Quiet
   until pointed at: it is a way to somewhere else, and the row below it is the thing being used. */
.mark-label
{
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: .5rem;
}

.mark-palette-edit
{
	display: inline-flex;
	padding: .15rem;
	border-radius: .25rem;
	color: var( --ink-quiet );
}

.mark-palette-edit:hover { color: var( --ink ); background: var( --selection-background ); }
.mark-palette-edit:focus-visible { outline: 2px solid var( --accent ); outline-offset: 1px; }
.mark-palette-edit .icon { width: .9rem; height: .9rem; }


/* The legend view. Rows of fields, one form per section. */
.legend-section { margin: 1.6rem 0 0; max-width: 52rem; }

.legend-section-heading
{
	margin: 0 0 .5rem;
	font-size: var( --text-md );
}

.legend-rows,
.legend-key
{
	margin: .6rem 0 .9rem;
	padding: 0;
	list-style: none;
}

.legend-key
{
	display: flex;
	flex-wrap: wrap;
	gap: .45rem .9rem;
}

.legend-key-link { text-decoration: none; }
.legend-key-link:hover .legend-word { text-decoration: underline; }

.legend-row
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .4rem .7rem;
	padding: .4rem 0;
	border-bottom: 1px solid var( --rule );
}

/* The sample stands in a fixed-width slot so the fields after it line up row to row, and is NOT
   itself stretched to the slot: a pill eleven rem wide reads as a progress bar. */
.legend-row > .legend-sample { flex: 0 0 11rem; min-width: 0; }

.legend-row .legend-carriers { flex: 0 0 6.5rem; font-size: var( --text-xs ); }
.legend-row .legend-word-field { flex: 1 1 10rem; min-width: 0; }
.legend-row .legend-position-field { flex: none; width: 3.6rem; }
.legend-row .legend-emoji-field { flex: none; width: 4.2rem; text-align: center; }
.legend-row select { flex: none; }

.legend-template-preview
{
	display: flex;
	flex-wrap: wrap;
	gap: .3rem .45rem;
}

.legend-remove-ladder { margin-top: 1.2rem; }

.details-state { margin: 0 0 1rem; }


/* A PERSON'S OWN MARKS (D434, lib/marks.php). The corner is the person's channel and the legend line
   is the team's, and they are never drawn in one place: a corner of the preview, where nothing of
   the team's is, on a small plate so an emoji stays readable over any picture. The TOP LEFT since
   2026-09-22 -- it was the right, until the kind badge took that corner (its rule says why); the
   state stripe down the left edge is the team's, but it is an edge and not a corner, and the two do
   not touch. */
.card-marks
{
	position: absolute;
	z-index: 2;
	top: .45rem;
	left: .45rem;
	display: inline-flex;
	gap: .15rem;
	padding: .1rem .3rem;
	border-radius: 999px;
	background: color-mix( in srgb, var( --card-background ) 86%, transparent );
	font-size: var( --text-md );
	line-height: 1.2;
	pointer-events: auto;
}

.card-mark-more { font-size: var( --text-2xs ); color: var( --ink-quiet ); align-self: center; }

/* In the list the corner is the THUMBNAIL's corner, and it stays absolutely placed. The first
   attempt gave it a grid cell at the end of the title, and because the row's cells are placed by
   the grid in document order, one extra cell pushed the title and every column after it one place
   along -- on marked rows only, so the table came apart exactly where a person was looking (seen on
   a screen, 2026-09-19). An overlay is never a cell. */
[data-library-layout="list"] .card-marks
{
	top: .05rem;
	right: auto;
	left: .25rem;
	gap: 0;
	padding: 0 .15rem;
	font-size: var( --text-2xs );
}

[data-library-layout="grid"] .card-marks { font-size: var( --text-sm ); top: .25rem; left: .25rem; }

.mark-palette
{
	display: flex;
	flex-wrap: wrap;
	gap: .2rem;
}

.mark-form { margin: 0; }

.mark-button
{
	width: 2.1rem;
	height: 2.1rem;
	padding: 0;
	border: 1px solid transparent;
	border-radius: 50%;
	background: none;
	font-size: var( --text-lg );
	line-height: 1;
	cursor: pointer;
}

.mark-button:hover { border-color: var( --rule ); }
.mark-button[aria-pressed="true"] { border-color: var( --ink ); background: var( --selection-background ); }
.mark-button:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }


/* ---------------------------------------------------------------------------------------------
   THE ICON LAYOUT (D452): a small picture and a name, as a file system draws a folder.

   The fourth value of `library_layout`, and the DENSE end of it. Cards are for looking at content,
   the grid for looking at pictures and the list for comparing metadata; this one is for HANDLING --
   the view in which forty things can be seen, marqueed and dragged onto three folders that are on
   the same screen. It is scoped to [data-library-layout="icons"] on <body>, exactly as the list and
   the grid are, so the markup is the card's own and nothing in PHP knows this layout exists except
   the page size (shell/listing.php, resolveListingPageSize()).

   WHAT IS DRAWN, and the list is short on purpose: the key image cropped square, or the family's
   glyph over the file's extension where there is no picture; the family's glyph as a corner badge
   on a picture that is not a still, because a small poster does not say "video"; the title on two
   lines; and the state stripe (D433), the one exclusive channel, which costs no room. Everything
   else a card says -- person, trail, legend words, meta, indicators, marks, the star -- is one
   keypress away in Cards, and is NOT squeezed in here at a size nobody can read.

   THE PICTURE'S SIZE IS THE PERSON'S (D562, Jon 2026-09-28), 32 to 128px, 64 unless they say: a
   slider in the toolbar per view, a preferred size in Settings. It was a fixed 44px (D452, "the touch
   minimum"); the smallest size now goes below that, and the cell a finger presses still does not --
   the name under the picture and the padding round it keep it at least 88px wide. `--icon-layout-size` is
   ALWAYS on <body>, written by the shell and re-written by app.js per view, and there is no fallback
   here on purpose: the default is PHP's (preferences.php, DEFAULT_ICON_SIZE). Everything below that
   scales -- the column, the glyphs, the state stripe -- is measured from it. It is a length in px and
   not rem deliberately: the text beside it follows the type scale, and the picture is a picture.
   --------------------------------------------------------------------------------------------- */

[data-library-layout="icons"] .app-main
{
	--icon-cell-picture: var( --icon-layout-size );
}

/* The column is the picture plus room for a two-line name: 100px at the old 44, as it was. */
[data-library-layout="icons"] .item-grid,
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] )
{
	grid-template-columns: repeat( auto-fill, minmax( calc( var( --icon-cell-picture ) + 3.5rem ), 1fr ) );
	gap: .35rem .25rem;
	align-items: start;
}

[data-library-layout="icons"] .item-card
{
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: .3rem;
	padding: .45rem .3rem .4rem;
	border: 0;
	border-radius: .5rem;
	background: none;
	box-shadow: none;
	/* The card clips to its rounded corners; a cell has nothing to clip and a kebab that sits on its
	   edge, so it does not. */
	overflow: visible;
}

[data-library-layout="icons"] .item-card:hover,
[data-library-layout="icons"] .item-card[data-selected="true"]
{
	background: var( --selection-background );
}

[data-library-layout="icons"] .item-card .card-preview
{
	flex: 0 0 auto;
	width: var( --icon-cell-picture );
	height: var( --icon-cell-picture );
	aspect-ratio: 1;
	border: 1px solid var( --rule );
	border-radius: .35rem;
	background: var( --card-background );
}

/* A crop somebody drew for a 16:9 card (D365) is not a crop for a square; centre and cover. */
[data-library-layout="icons"] .item-card .card-preview[data-key-crop] img
{
	object-position: center;
	transform: none;
}

/* THE FAMILY GLYPH, twice. Over a picture it is a corner badge -- glyph only, the words gone -- and
   a still gets none, because a picture of a picture needs no label. Where there is no picture it IS
   the icon: centred, larger, with the extension under it. */
[data-library-layout="icons"] .item-card .card-type
{
	--icon-size: max( .7rem, calc( var( --icon-cell-picture ) * .16 ) );
	top: auto;
	right: auto;
	left: -1px;
	bottom: -1px;
	gap: 0;
	padding: .1rem;
	border-radius: 0 .3rem 0 .3rem;
	background: var( --card-background );
	font-size: 0;
	letter-spacing: 0;
	backdrop-filter: none;
}

[data-library-layout="icons"] .item-card .card-type-suffix,
[data-library-layout="icons"] .item-card .card-type-fact,
[data-library-layout="icons"] .item-card[data-kind="image"] .card-preview:has( img ) .card-type
{
	display: none;
}

[data-library-layout="icons"] .item-card .card-preview:not( :has( img ) ) .card-type
{
	--icon-size: calc( var( --icon-cell-picture ) * .48 );
	inset: 0 0 auto 0;
	height: 68%;
	justify-content: center;
	padding: 0;
	border-radius: 0;
	background: none;
}

[data-library-layout="icons"] .item-card .card-preview .no-preview
{
	position: absolute;
	inset: auto 0 .15rem 0;
	width: auto;
	height: auto;
	padding: 0 .1rem;
	font-size: 0;
}

[data-library-layout="icons"] .item-card .no-preview-noun { display: none; }

[data-library-layout="icons"] .item-card .no-preview-extension
{
	font-size: var( --text-2xs );
	letter-spacing: .02em;
	color: var( --ink-quiet );
}

/* Uploading or processing: the sentence does not fit and the fact still matters, so the pill
   becomes a dot in the picture's corner. The title attribute-less dot is a hint, not the report --
   the queue tray is the report. */
[data-library-layout="icons"] .item-card .card-progress
{
	inset: auto -.2rem -.2rem auto;
	width: .7rem;
	height: .7rem;
	padding: 0;
	border-radius: 50%;
	border: 2px solid var( --page-background );
	font-size: 0;
	box-shadow: none;
}

[data-library-layout="icons"] .item-card .card-body
{
	display: block;
	width: 100%;
	min-width: 0;
	padding: 0;
	text-align: center;
}

[data-library-layout="icons"] .item-card .card-title
{
	/* The card keeps room beside its title for the star and the kebab. Here the star is gone and the
	   kebab sits over the cell's corner, and that room is half the cell. */
	padding: 0;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
	font-size: var( --text-xs );
	font-weight: 500;
	line-height: 1.25;
	overflow-wrap: anywhere;
	white-space: normal;
}

[data-library-layout="icons"] .item-card .card-context,
[data-library-layout="icons"] .item-card .card-person,
[data-library-layout="icons"] .item-card .card-legend,
[data-library-layout="icons"] .item-card .card-meta,
[data-library-layout="icons"] .item-card .card-footer,
[data-library-layout="icons"] .item-card .indicators,
[data-library-layout="icons"] .item-card .pin-button,
[data-library-layout="icons"] .item-card .card-marks,
[data-library-layout="icons"] .item-list-head
{
	display: none;
}

/* The state stripe, beside the picture rather than down the edge of a cell that has no edge. */
[data-library-layout="icons"] .item-card[data-legend-state-color]::before
{
	top: .45rem;
	bottom: auto;
	left: calc( 50% - var( --icon-cell-picture ) / 2 - .45rem );
	width: .22rem;
	height: var( --icon-cell-picture );
	border-radius: .2rem;
}

[data-library-layout="icons"] .item-card[data-state="removed"]
{
	border: 0;
}

/* The kebab: the grid's answer (D427), for the grid's reason -- a visible control on every one of
   two hundred cells is noise, and a finger has no hover so it gets the control outright. */
[data-library-layout="icons"] .item-card .item-menu-button
{
	grid-row: auto;
	top: .15rem;
	right: .15rem;
	opacity: 0;
}

[data-library-layout="icons"] .item-card:hover .item-menu-button,
[data-library-layout="icons"] .item-card[data-selected="true"] .item-menu-button,
[data-library-layout="icons"] .item-menu-button:focus-visible,
[data-library-layout="icons"] .item-menu-button[aria-expanded="true"] { opacity: 1; }

@media ( hover: none )
{
	[data-library-layout="icons"] .item-card .item-menu-button { opacity: 1; }
}

/* CONTAINERS, DRAWN AS THE SAME KIND OF THING. A folder among its contents is one more icon with a
   name under it, first in the window, which is what a file system does and what makes a drop target
   sit beside the things being dropped. The large tiles of the Favorites rail are not touched. */
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .scope-tile
{
	flex-direction: column;
	gap: .3rem;
	padding: .45rem .3rem .4rem;
	border-color: transparent;
	background: none;
	text-align: center;
}

[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .scope-tile:hover
{
	background: var( --selection-background );
}

[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-glyph
{
	--icon-size: calc( var( --icon-cell-picture ) * .76 );
	justify-content: center;
	width: var( --icon-cell-picture );
	height: var( --icon-cell-picture );
}

[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-text
{
	width: 100%;
}

[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-name
{
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	/* The clamp clips only with this; see the large tile's copy of the same note. */
	overflow: hidden;
	font-size: var( --text-xs );
	font-weight: 500;
	line-height: 1.25;
	overflow-wrap: anywhere;
	white-space: normal;
}

/* WHERE IT IS survives, and nothing else of the meta line does (Jon, 2026-09-21: four tiles reading
   "Inbox" on the Recently used rail). A tile that states its place wraps it in `.tile-place`
   (shell/views.php, renderActivityTiles()); the counts and the activity ride in `.tile-meta-rest` and go.
   A tile drawn inside its own parent's page states no place and so draws no second line at all --
   there the page heading is the specifier. One line, quiet, ellipsed: it is a qualifier, not a name. */
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-meta
{
	display: block;
	margin-top: .1rem;
	font-size: var( --text-2xs );
	line-height: 1.2;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* A meta line with no place in it is all counts: gone whole, so it leaves no empty line behind. */
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-meta:not( :has( .tile-place ) ),
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .tile-meta-rest,
[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .pin-button
{
	display: none;
}

[data-library-layout="icons"] .app-main .scope-grid:not( [data-tile-size] ) .scope-tile[data-legend-state-color]::before
{
	top: .45rem;
	bottom: auto;
	left: calc( 50% - var( --icon-cell-picture ) / 2 - .45rem );
	width: .22rem;
	height: var( --icon-cell-picture );
	border-radius: .2rem;
}


/* ---------------------------------------------------------------------------------------------
   ADMINISTRATION > PEOPLE (Jon, 2026-09-24): the list's toolbar, the System Admin tick, and the
   Manage panel's sections.

   The toolbar is one row: Add somebody on the left, then Find and Show, the pager at the far end
   (.picker-pages' own `margin-left: auto`). The tools are the member picker's, and outside a
   .details-form their labels are plain inline boxes, so the caption-beside-field rule is said here
   rather than borrowed through a class that means "inside a form".
   --------------------------------------------------------------------------------------------- */
.people-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin: 0 0 .75rem; }
.people-toolbar > .picker-tools { flex: 1 1 32rem; }
.people-tools .picker-search { display: flex; align-items: center; gap: .5rem; font-size: var( --text-base ); }
.people-accounts > h2 { margin-bottom: .75rem; }

/* A checkbox and its words on one line, the box first. `.details-form label` is a grid (words over
   field), and the box's inherited field width centred it on a row of its own above its own label --
   the "System Admin (the whole installation)" that did not flow. Two classes and an element, so it
   outranks that rule rather than arguing with it. */
.details-form label.details-checkbox { display: flex; align-items: center; gap: .5rem; justify-self: start; }
.details-form label.details-checkbox > input { width: auto; margin: 0; flex: none; }

/* The Manage panel: four sections, each with its heading, parted by a hairline and spaced by one
   number, so nothing in it floats. The panel's dock (Cancel · Save) is the tool area's last child and
   sits under the last section at the same step. */
.user-panel { display: grid; }
.user-panel-section { padding-block: 1rem; border-top: 1px solid var( --rule ); }
.user-panel-section:first-child { padding-top: 0; border-top: 0; }
.user-panel-section > h3 { margin: 0 0 .6rem; font-size: var( --text-md ); }
.user-panel-section > .details-description { margin: 0 0 .6rem; }
.user-panel-section > :last-child { margin-bottom: 0; }
/* The dock stays on screen while the panel scrolls: somebody with sixty grants had Save a long
   scroll below the fields it saves. Sticky to the tool area's foot, on the box's own ground, and
   pulled over the box's bottom padding so nothing scrolls visibly beneath it. */
.user-panel + .scope-panel-dock
{
	position: sticky;
	bottom: -1.15rem;
	margin: 0 0 -1.15rem;
	padding: .75rem 0 1.15rem;
	border-top: 1px solid var( --rule );
	background: var( --card-background );
}

/* Name and Email, Status and Kind, in two columns; the tick, the notes and the Add panel's dock run
   the full width underneath. One column on a phone. */
.user-panel-fields { grid-template-columns: repeat( 2, minmax( 0, 1fr ) ); gap: .75rem 1rem; }
.user-panel-fields > :not( label ),
.user-panel-fields > label.details-checkbox { grid-column: 1 / -1; }
.user-panel-fields select { width: 100%; }

/* An email field at the text field's height and size: `.details-form input[type="text"]` names only
   the one type, so Email stood shorter than Name beside it. */
.user-panel-fields input[ type="email" ] { padding: .3rem .55rem; font: inherit; font-size: var( --text-md ); }

@media ( max-width: 40rem )
{
	.user-panel-fields { grid-template-columns: minmax( 0, 1fr ); }
}

/* Sign-in's three presses, in one row rather than three stacked forms a screen tall. */
.user-panel-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.user-panel-actions > form { margin: 0; }

/* Memberships: each grant a row, its words on the left and Revoke at the right-hand end on the same
   line; then the grant form as one row -- role, scope, Grant -- bottoms aligned, so the button sits on
   the fields' line and not on their captions'. */
.user-grant-list { list-style: none; margin: 0 0 .9rem; padding: 0; display: grid; }
.user-grant-list > li { display: flex; align-items: center; justify-content: space-between; gap: .75rem; min-height: var( --control-height ); padding-block: .2rem; border-bottom: 1px solid var( --rule ); }
.user-grant-list > li:first-child { border-top: 1px solid var( --rule ); }
.user-grant-revoke { margin: 0; flex: none; }
.user-grant-add-heading { margin: 0 0 .5rem; font-size: var( --text-base ); font-weight: 600; }
.user-grant-form { display: grid; grid-template-columns: minmax( 0, 2fr ) minmax( 0, 3fr ) auto; align-items: end; gap: .5rem .75rem; margin: 0 0 .6rem; }
.user-grant-form > label { display: grid; gap: .25rem; min-width: 0; font-size: var( --text-base ); }
.user-grant-form select { width: 100%; }

@media ( max-width: 40rem )
{
	.user-grant-form { grid-template-columns: minmax( 0, 1fr ); justify-items: stretch; }
	.user-grant-form > .control-pill { justify-self: end; }
}
.user-removal-form { display: grid; gap: .6rem; justify-items: start; }


/* ---------------------------------------------------------------------------------------------
   THE SERVICE REGION (D508): the availability strip and the announcement, above the whole shell.

   The one banner allowed to move the page. ui-design's rule is that an outcome is a toast and never
   pushes the view; this is not an outcome, it is the installation interrupting on purpose (Jon,
   2026-09-24: "this is meant to disrupt so it can block ui"). While the region has content the body
   becomes a column and the shell takes what is left, so the footer stays on screen; with nothing in
   the region nothing here applies and the shell is the 100dvh grid it always was. Tokens only, so
   every theme colours it without knowing it exists.
   --------------------------------------------------------------------------------------------- */

.service-region:empty { display: none; }

body.app-shell-body:has( > .service-region:not( :empty ) ) { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
body.app-shell-body:has( > .service-region:not( :empty ) ) > .app-shell { flex: 1 1 auto; min-height: 0; height: auto; }

.service-region { position: relative; z-index: 30; }

.service-strip,
.service-announcement
{
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: .25rem .9rem;
	padding: .55rem 1rem;
	padding-top: max( .55rem, env( safe-area-inset-top, 0px ) );
	border-bottom: 1px solid var( --rule );
	font-size: var( --text-base );
}

.service-strip p,
.service-announcement p { margin: 0; }

.service-strip[data-tone="warning"],
.service-announcement[data-level="warning"] { background: var( --warning-quiet ); color: var( --ink ); }

.service-strip[data-tone="critical"] { background: var( --danger-quiet ); color: var( --ink ); }

.service-announcement[data-level="info"] { background: var( --accent-quiet ); color: var( --ink ); }

.service-strip-headline strong { color: var( --warning ); }
.service-strip[data-tone="critical"] .service-strip-headline strong { color: var( --danger ); }
.service-strip-back { margin-left: .5rem; color: var( --ink-quiet ); }
.service-strip-link { margin-left: .5rem; }
.service-strip-message,
.service-announcement-message { flex: 1 1 20rem; min-width: 0; }
.service-announcement-message > :first-child { margin-top: 0; }
.service-announcement-message > :last-child { margin-bottom: 0; }

.service-dismiss { margin: 0 0 0 auto; }

.service-dismiss-button
{
	min-width: 2rem;
	min-height: 2rem;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: inherit;
	font: inherit;
	font-size: var( --text-lg );
	line-height: 1;
	cursor: pointer;
}

.service-dismiss-button:hover,
.service-dismiss-button:focus-visible { background: color-mix( in srgb, var( --ink ) 10%, transparent ); }

/* A critical notice: modal until acknowledged, then a thin bar holding a pill that reopens it. The
   pill is drawn in both states and shown only once dismissed, so the client never builds it. A bar in
   the flow and not a floater: pinned to a corner it sat on the toolbar's Upload button, and a notice
   that covers a control is a second problem. One line tall, so it costs little of the page. */
.service-critical-pill { display: none; }

.service-critical[data-dismissed="true"]
{
	display: flex;
	justify-content: flex-end;
	padding: .25rem 1rem;
	padding-top: max( .25rem, env( safe-area-inset-top, 0px ) );
	border-bottom: 1px solid var( --rule );
	background: var( --danger-quiet );
}

.service-critical[data-dismissed="true"] .service-critical-pill
{
	display: inline-flex;
	align-items: center;
	gap: .35rem;
	padding: .15rem .8rem;
	border: 1px solid var( --danger );
	border-radius: 999px;
	background: var( --card-background );
	color: var( --danger );
	font: inherit;
	font-size: var( --text-sm );
	cursor: pointer;
}

dialog.service-critical-dialog,
dialog.service-reload-dialog
{
	width: min( 36rem, calc( 100vw - 2rem ) );
	padding: 1.25rem 1.4rem;
	border: 1px solid var( --rule );
	border-top: 4px solid var( --danger );
	border-radius: 10px;
	background: var( --card-background );
	color: var( --ink );
}

dialog.service-reload-dialog { border-top-color: var( --accent ); }

dialog.service-critical-dialog h2,
dialog.service-reload-dialog h2 { margin: 0 0 .6rem; font-size: var( --text-lg ); }

dialog.service-critical-dialog .service-dismiss,
dialog.service-critical-dialog .service-critical-close { display: flex; justify-content: flex-end; margin: 1rem 0 0; }

dialog.service-critical-dialog::backdrop,
dialog.service-reload-dialog::backdrop { background: rgba( 16, 18, 20, .55 ); }

.badge[data-tone="danger"] { background: var( --danger-quiet ); color: var( --danger ); }

/* The Service page itself. */
.service-state-line { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .7rem; margin: 0; font-size: var( --text-md ); }
.service-current { display: grid; gap: .6rem; max-width: 46rem; margin-bottom: 1.2rem; }
.service-current p { margin: 0; }
.service-preview { border: 1px solid var( --rule ); border-radius: 8px; }
.details-form fieldset.service-choice { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem 1.1rem; margin: 0; padding: 0; border: 0; min-width: 0; }
.details-form fieldset.service-choice legend { float: left; width: 100%; margin: 0 0 .3rem; padding: 0; font-size: var( --text-sm ); color: var( --ink-quiet ); }
.details-form fieldset.service-choice > label { display: flex; align-items: center; gap: .4rem; white-space: nowrap; }
.details-form fieldset.service-choice > p { flex-basis: 100%; margin: .1rem 0 0; font-size: var( --text-sm ); }
.details-form input[ type="datetime-local" ] { justify-self: start; padding: .3rem .5rem; font: inherit; }
.details-form label > .quiet { font-size: var( --text-sm ); }
.service-reopen { margin: 0 0 0 auto; }

/* WAITING FOR FILE (D522) on a card: the processing indicator's own accent, and the picture area
   says so in words where a poster would be, because a placeholder has no extension to show either. */
.item-card .card-waiting
{
	display: grid;
	place-items: center;
	height: 100%;
	color: var( --ink-quiet );
	font-size: var( --text-sm );
	letter-spacing: .02em;
}


/* A WEB LINK (D561) where a picture would be: the family's glyph over the site's name. Buffet's own
   placeholder, for a page that offered no picture or has not been read yet. The name is the fact a
   person opens a link by, so it takes the ink and the glyph stays quiet. Used on the card and, at
   the item page's size, in the link's own area (item.css). */
.no-preview.card-link
{
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: .3em;
	width: 100%;
	height: 100%;
	padding: 0 .8em;
}

.card-link-glyph
{
	font-size: 2.1em;
	line-height: 1;
	color: var( --ink-quiet );
}

.card-link-site
{
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var( --ink );
	font-size: var( --text-base );
	font-weight: 650;
}

/* ...and in the badge, where a file's suffix would be. A suffix is capitals; a site is written as it
   is, because `BBC.COM` is not what anybody types. */
.card-type-suffix[data-kind="site"]
{
	text-transform: none;
	letter-spacing: 0;
}

/* NEW ITEM's Address field (D561), shown when the kind is Web link. `:has()` does it, so the form
   works with script off; a browser without `:has()` fails the @supports test and keeps the field
   on show, which is the safe way round -- a field that cannot be reached would make a link
   impossible to create. The server decides what the form must carry either way. */
@supports selector( :has( * ) )
{
	.details-form[data-scope-new-item] .new-item-address
	{
		display: none;
	}

	.details-form[data-scope-new-item]:has( select[name="family"] option[value="link"]:checked ) .new-item-address
	{
		display: grid;
	}
}

.details-form .new-item-address .field-hint
{
	color: var( --ink-quiet );
	font-size: var( --text-sm );
}


/* A file over an item card (D545, C171): the card fills like every other "into" target, in the drag
   vocabulary's own words (the MOVING THINGS block). The dialog that asks before the file is taken is
   the mover's shape -- a plain box over a dimmed shell, the question, then the two verbs. */
.item-card[data-drop-state="into"]
{
	outline: 2px solid var( --accent );
	outline-offset: 2px;
	border-radius: 8px;
}

.upload-replace-dialog
{
	max-width: min( 32rem, calc( 100vw - 2rem ) );
	padding: 1.25rem 1.5rem;
	border: 1px solid var( --rule );
	border-radius: 10px;
	background: var( --card-background );
	color: var( --ink );
}

.upload-replace-dialog::backdrop { background: rgba( 16, 18, 20, .45 ); }
.upload-replace-question { margin: 0 0 .5rem; font-size: var( --text-md ); font-weight: 600; }
.upload-replace-dialog .quiet { margin: 0 0 1rem; }
.upload-replace-actions { display: flex; justify-content: flex-end; gap: .5rem; margin: 0; }


/* Items beneath a place that a newer recipe version could process again (D551): the admin's quiet
   badge on a sidebar row, beside the count, and the verb's form inside the scope page's notice. */
.row-renew
{
	margin-left: .35rem;
	font-size: var( --text-xs );
	color: var( --accent );
	white-space: nowrap;
}

/* A notice with an action (renderNotice()'s fifth argument): the words take the room, the button sits
   at the banner's end on the same line, and on a narrow screen it wraps under the words, still inside. */
.notice:has( > .notice-action ) { flex-wrap: wrap; align-items: center; }
/* The sentence in the body's ink (Jon, 2026-09-29: the accent tint read "a little too dreamy"); the
   glyph and the button keep the tone, so the band still says what kind of notice it is. */
.notice > .notice-text { flex: 1 1 16rem; min-width: 0; color: var( --ink ); }
.notice > .notice-action { flex: 0 0 auto; margin-left: auto; }
.notice > .notice-action form { margin: 0; }


/* A pane on a phone must never be wider than the screen (Jon, 2026-09-26: the podcast pane floated
   sideways). Two things let a pane grow past it: a grid track with `min-width: auto`, which widens
   to its widest item instead of shrinking it, and a <select> whose minimum is its widest option --
   the cover picker's "{title} (in {folder})" is as long as a title. So the form's tracks are
   minmax( 0, 1fr ), controls are capped at their track, and the scrolling pane clips sideways. */
.details-form,
.details-form label { grid-template-columns: minmax( 0, 1fr ); }

.details-form select,
.details-form input,
.details-form textarea,
dialog.scope-panel select,
.action-panel-dialog select { max-width: 100%; min-width: 0; box-sizing: border-box; }

dialog.scope-panel > .tool-area,
.action-panel-dialog .folder-action-panel { overflow-x: hidden; }

/* "View" over the rung chips, "Download" over the download rows (Jon, 2026-09-26): the two rows of
   pills in a Web video or Web renditions area do different things and looked alike. The same weight
   as the embed block's title beneath them, so the area reads as three named parts. */
.tool-area-subheading
{
	margin: .9rem 0 .35rem;
	font-size: var( --text-sm );
	font-weight: 600;
	color: var( --ink );
}

.tool-area-subheading + .renditions { margin-top: 0; }


/* SOMETHING ON THIS ROW IS WAITING FOR THE READER (D556): a task, where `.row-count` is a fact. Its
   first caller is Administration > People, for erasure requests. Filled, so that it reads at the
   width of a sidebar without being read as the count of what the row contains. */
.row-attention
{
	margin-left: .35rem;
	min-width: 1.25em;
	padding: 0 .4em;
	border-radius: 1em;
	background: var( --warning );
	color: var( --page-background );
	font-size: var( --text-xs );
	text-align: center;
	white-space: nowrap;
}


/* ---------------------------------------------------------------------------------------------
   THE PEOPLE LIST, AS ONE TABLE (D610), and the invitation's fields in its picker.

   The picker opens at the TOP of the form now, so it wants air under it before the table begins.
   --------------------------------------------------------------------------------------------- */
.membership-form > [data-member-picker] { margin-bottom: 1rem; }

/* A name stays on one line. On a phone the three columns squeezed it into "Jon / Fabritius" over
   two lines beside its chip; the table scrolls sideways there already (.table-scroll), and a name
   that reads at a glance is worth more than a role sentence that fits. */
.people-access-table .person-identity { white-space: nowrap; }

/* A member whose stronger access comes from elsewhere says so on a second line of the same cell. */
.people-access-also { display: block; color: var( --ink-quiet, inherit ); font-size: var( --text-sm ); }

/* The invitation's fields: a quiet box under the list, the width of a form and not of the table.
   `[hidden]` is restated for the reason .table-load-more[hidden] is, a few thousand lines up. */
.picker-invite
{
	display: grid;
	gap: .6rem;
	max-width: 34rem;
	margin-top: .75rem;
	padding: .9rem 1rem 1rem;
	border: 1px solid var( --rule );
	border-radius: 0.5rem;
}
.picker-invite[hidden] { display: none; }
.picker-invite > .control-pill { justify-self: start; }
.picker-invite .details-description { margin: 0; }
.picker-invite-lead { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

/* The invitation's own form holds hidden fields and nothing to draw. */
.invitation-form { display: contents; }


/* THE SEARCH PAGE'S QUERY LINE AND ITS CHIPS (D674 §5b.1, D678): the query as typed in a full-width
   field, and beneath it one removable pill per typed token. The pill is .control-pill; what is added
   is the × beside the label, drawn as the pill's own second half. */
.search-query-line { display: flex; gap: .5rem; align-items: center; margin: 0 0 .75rem; }
.search-query-field { flex: 1 1 auto; min-width: 0; font: inherit; font-size: var( --text-md ); padding: .35rem .75rem; border: 1px solid var( --rule ); border-radius: 999px; background: var( --card-background ); color: var( --ink ); min-height: var( --control-height ); }
.search-query-field:focus-visible { outline: 2px solid var( --accent ); outline-offset: 2px; }
.search-chips { display: flex; flex-wrap: wrap; gap: .4rem; list-style: none; margin: 0 0 1rem; padding: 0; }
.search-chip { gap: .45rem; padding-right: .35rem; cursor: default; }
.search-chip-remove { display: inline-flex; align-items: center; justify-content: center; width: 1.3rem; height: 1.3rem; border-radius: 999px; color: var( --ink-quiet ); text-decoration: none; }
.search-chip-remove:hover, .search-chip-remove:focus-visible { background: var( --rule ); color: var( --ink ); }

/* THE FACET STRIP (D674 §5b.2, D679): a row of <details>, each a pill that opens a list of values
   with counts. The open list floats under its pill so the strip keeps one height. The strip and the
   Group by switch are two rows of one grid (D683): a label column of one width, so the controls of
   both begin on one vertical line and the labels sit centred on their pills. */
/* The label column is as wide as its words and the labels sit on the pills' baseline (Jon: "a bit far
   and low"); `subgrid`-free, so both rows share one width through `max-content` on the same text. */
.search-refine-row { display: grid; grid-template-columns: 5.2rem minmax( 0, 1fr ); align-items: baseline; column-gap: .6rem; margin: 0 0 .6rem; }
.search-refine-label { font-size: var( --text-sm ); color: var( --ink-quiet ); }
@media ( max-width: 40rem ) { .search-refine-row { grid-template-columns: 1fr; row-gap: .25rem; } }
.search-facets { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; position: relative; }
.search-facet { position: relative; }
.search-facet > summary { list-style: none; }
.search-facet > summary::-webkit-details-marker { display: none; }
.search-facet > summary::after { content: " ▾"; opacity: .6; }
.search-facet[open] > summary { border-color: var( --ink-quiet ); }
.search-facet-values { position: absolute; z-index: 5; top: calc( 100% + .3rem ); left: 0; min-width: 14rem; max-width: 24rem; max-height: 20rem; overflow: auto; margin: 0; padding: .35rem; list-style: none; border: 1px solid var( --rule ); border-radius: .6rem; background: var( --card-background ); box-shadow: 0 .5rem 1.5rem rgba( 0, 0, 0, .12 ); }
.search-facet-value { display: flex; justify-content: space-between; gap: 1rem; padding: .3rem .5rem; border-radius: .4rem; color: var( --ink ); text-decoration: none; font-size: var( --text-sm ); }
.search-facet-value:hover, .search-facet-value:focus-visible { background: var( --rule ); }
.search-facet-value .count { color: var( --ink-quiet ); font-variant-numeric: tabular-nums; }
.search-facets-note { font-size: var( --text-xs ); color: var( --ink-quiet ); }

/* GROUP BY and THE MATCH LINE (D681). The switch is the second refine row; a grouped section has its own
   heading; the match line is one quiet line under a card's title with the found words marked. */
.search-group-switch { margin-bottom: 1rem; }
.search-group-switch .view-switch { justify-self: start; }
/* Always visible beside a heading: there is no row to hover. Quiet until pinned, gold once it is;
   a little room from the title and on its centre line (Jon: "a bit high and close"). */
.view-heading-pin { align-self: center; margin-left: .35rem; }
.view-heading-pin .pin-button { font-size: var( --text-lg ); line-height: 1; opacity: 1; transform: translateY( .08em ); }
.search-group > h3 { font-size: var( --text-md ); margin: 1rem 0 .5rem; }
.search-group > h3 .count { color: var( --ink-quiet ); font-weight: normal; margin-left: .35rem; }
.card-match { margin: .15rem 0 .35rem; font-size: var( --text-xs ); color: var( --ink-quiet ); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.card-match-where { font-variant: small-caps; margin-right: .3rem; }
.card-match mark { background: color-mix( in srgb, currentColor 18%, transparent ); color: inherit; border-radius: .2rem; padding: 0 .1rem; }

/* THE WAY IN (D674 §5e, D684): a magnifier in the sidebar field's empty slot, where the × stands when there
   is text -- the same `:placeholder-shown` rule, drawn the other way -- opening the search page. */
.sidebar-search .search-open
{
	position: absolute;
	top: 50%;
	right: calc( .9rem + .3rem );
	transform: translateY( calc( -50% + 2px ) ); /* two pixels lower than the ×, on the text's line (Jon) */
	margin-top: -.275rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: calc( var( --icon-button-size ) * .8 );
	height: calc( var( --icon-button-size ) * .8 );
	border-radius: .3rem;
	color: var( --ink-quiet );
	text-decoration: none;
}
.sidebar-search .search-open:hover, .sidebar-search .search-open:focus-visible { color: var( --ink ); background: var( --rule ); }
.sidebar-search input:not( :placeholder-shown ) ~ .search-open { display: none; }

/* ITEMS UPLOADED BY (D687): the pivot under a person's search tile, outside its anchor. */
.scope-tile-item .tile-pivot { position: absolute; right: .5rem; bottom: .5rem; z-index: 2; font-size: var( --text-xs ); min-height: 1.6rem; padding: .1rem .55rem; }
.scope-tile-item:has( .tile-pivot ) .scope-tile { padding-bottom: 2.4rem; }

/* THE ZERO-RESULTS GUIDE (D688): the way back from a search that found nothing, as pills with counts. */
.search-zero-guide { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: .5rem 0 0; }
.search-zero-label { font-size: var( --text-sm ); color: var( --ink-quiet ); margin-right: .2rem; }
.search-zero-option .count { color: var( --ink-quiet ); margin-left: .15rem; }

/* PIVOTS ON A CARD (D690): three quiet links under the match line, each one more chip. */
.card-pivots { display: flex; flex-wrap: wrap; gap: .25rem .6rem; margin: 0 0 .35rem; font-size: var( --text-xs ); }
.card-pivot { color: var( --ink-quiet ); text-decoration: none; border-bottom: 1px dotted var( --rule ); }
.card-pivot:hover, .card-pivot:focus-visible { color: var( --ink ); border-bottom-color: var( --ink-quiet ); }

/* ON A PHONE the facet's list flows under its pill across the row instead of floating (D690): a sheet
   in all but name, and nothing to clip at the window's edge. */
@media ( max-width: 40rem )
{
	.search-facets { flex-direction: column; align-items: stretch; }
	.search-facet-values { position: static; min-width: 0; max-width: none; max-height: 14rem; box-shadow: none; margin: .25rem 0 .35rem; }
}

/* A transcript hit's moment (D691): the match line is a link to the player at that second. */
.card-match-moment { color: inherit; text-decoration: none; }
.card-match-moment:hover .card-match-where, .card-match-moment:focus-visible .card-match-where { text-decoration: underline; }
