/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/*
 * Plus Jakarta Sans, self-hosted rather than linked to Google's CDN — the two
 * subsets below (latin, latin-ext) are the ones this app's copy actually
 * needs. A variable font, so one file covers the whole weight range instead
 * of one download per weight. Propshaft digests and serves these like any
 * other asset, which is what keeps them cached offline the same way; the
 * relative url()s below are rewritten to the digested path at asset-compile
 * time. `font-display: swap` means a page never blocks on this — the system
 * fallback in --font-sans shows first and swaps in once this arrives.
 */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/assets/plus-jakarta-sans-latin-d2ff26bc.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/assets/plus-jakarta-sans-latin-ext-ff4456f1.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

/*
 * No colour, radius or shadow is declared in this file. Every one of them is
 * a theme token in app/assets/tailwind/application.css, and Tailwind's build
 * publishes each as a real custom property on :root — so a rule below writes
 * `var(--color-slate-200)` and gets exactly the value `border-slate-200`
 * gives a template. They were written out a second time here once, under
 * shorter names, and a shorter name is not worth two places to change a
 * colour: the copies drifted from what the utilities were actually drawing.
 *
 * The roles, in the order this file uses them: 900 is text, 600 the quieter
 * text beside it, 400 a hint, 200 a line, 100 a panel face. `accent` is the
 * one action worth doing on a screen, and `accent-ink` reads on top of it.
 *
 * A `var()` here therefore has no fallback on purpose. The token block is
 * `@theme static`, so every one of them is in the build whether or not a
 * utility class happens to use it, and a fallback would only be a third copy
 * of the value waiting to go stale.
 */

/*
 * A preview panel (see below) sits `position: absolute; left: 100%` of its
 * trigger and is only ever hidden with `visibility`, never `display` — that
 * is what lets it fade in over a transition instead of popping. But
 * `visibility: hidden` still occupies box space, and nothing between it and
 * the page root clips that space, so on any screen narrower than the
 * sidebar's own width plus a hidden panel's width, the panel — invisible
 * and unreachable — forced the whole page to scroll sideways to make room
 * for it anyway. This is what keeps a panel's off-screen space from ever
 * becoming the page's problem.
 *
 * `clip`, not `hidden`, and the difference is load-bearing. `overflow-x:
 * hidden` computes `overflow-y` to `auto`, which makes <body> a scroll
 * container that never scrolls (its height is its content) — and a
 * `position: sticky` child of it then sticks to a box that is not the one
 * the reader scrolls, so it scrolls away instead of holding. `clip` clips
 * the same off-screen space without becoming a scroll container, so the
 * viewport stays the thing that scrolls and the sticky page-head on a phone
 * holds the top. See `.page-head` under `@media (max-width: 40rem)`.
 */
html,
body {
  overflow-x: clip;
}

/*
 * Android Chromium paints a grey box over anything tappable for a moment when
 * a finger lands on it. The app already answers a tap its own way — a button
 * lifts, a field draws its focus ring, a write shows the writing bar — so the
 * grey flash is a second, cruder answer on top of the designed one, and it is
 * the tell of a page that was never dressed for a phone. iOS does not draw it
 * and a mouse never triggers it, so this is Android's alone to lose.
 */
html {
  -webkit-tap-highlight-color: transparent;
}

/*
 * Hints — the small "?" beside a label. See HintsHelper.
 *
 * Deliberately not Tailwind's hover: variant: that is wrapped in
 * @media (hover: hover), so it never applies on a touch screen. Focus is the
 * way in there, and both paths belong in the same rule.
 */
.hint > .hint-bubble {
  visibility: hidden;
  opacity: 0;
  transition: opacity 120ms ease-out;
}

.hint:hover > .hint-bubble,
.hint:focus-within > .hint-bubble {
  visibility: visible;
  opacity: 1;
}

/*
 * Previews — a peek at what is inside the thing under the pointer. See
 * PreviewsHelper. Same reasoning as hints: hover and focus belong in one
 * rule, because focus is the only way in on a touch screen and for anyone
 * moving through the page by keyboard.
 */
.preview > .preview-panel {
  visibility: hidden;
  opacity: 0;
  transition: opacity 120ms ease-out;
}

.preview:hover > .preview-panel,
.preview:focus-within > .preview-panel {
  visibility: visible;
  opacity: 1;
  /* The panel is pointer-events-none so a hidden box never steals a click.
     Once it is showing, a name inside it is a link and has to take one. */
  pointer-events: auto;
}

/*
 * Page regions.
 *
 * Every page is the same four areas in the same order, so a page you have
 * never seen still tells you where to look: where you are, what this is, what
 * you can do about it, and then the thing itself.
 *
 *   .page
 *     .page-head      where you are, what this is, what you can do
 *     .page-strip     the controls belonging to this page — tabs, filters
 *     .page-body      the content
 *
 * Written as CSS rather than repeated utility classes because the regions are
 * a fixed vocabulary: naming them once is what stops the next page inventing
 * its own spacing.
 */

.page {
  max-width: 96rem;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 4rem;
}

.page--narrow { max-width: 36rem; }

/* A listing uses the main column. A table with nine fields, or two
   type cards on a hunt, was sitting in a 72rem well with empty
   ground either side. Settings and a how-to stay on `.page`. */
.page--spread { max-width: none; }

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--color-slate-200);
}

.page-head__where {
  flex-basis: 100%;
  font-size: var(--text-caption);
  color: var(--color-slate-600);
}
.page-head__where a { color: inherit; text-decoration: none; }
.page-head__where a:hover { color: var(--color-slate-900); }

.page-title {
  margin: 0;
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--color-slate-900);
}

.page-head__note {
  flex-basis: 100%;
  font-size: 0.8125rem;
  color: var(--color-slate-600);
}

.page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  font-size: 0.875rem;
}

.page-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  border-bottom: 1px solid var(--color-slate-200);
  background: var(--color-slate-100);
  margin: 0 -1.5rem;
  padding: 0 1.5rem;
}

.page-body { padding-top: 1rem; }

/* On a phone the menu bar scrolls away with the page, and the page-head is
 * what holds the top — so the page's name and its one action stay in reach
 * while a long list scrolls under them, rather than being a scroll back to the
 * top away. Off on a wider screen, where the whole head is on screen already
 * and a second held strip would only cost height.
 *
 * It gets a background, because it now draws over the rows as they pass; the
 * border it already carries is the line under it. `top` is the notch inset, so
 * with the menu bar gone the head pins clear of the camera. Full width to the
 * screen edge — the page's own 1.5rem gutter handed back as padding, the way
 * `.page-strip` does it — so no row shows at its side. Under the menus and
 * their panels (z 40 and up), over the rows.
 *
 * Not the sign-in card or the front page: neither is a list to scroll, and
 * both draw the head their own way. */
@media (max-width: 40rem) {
  .page:not(.auth-page):not(.landing) > .page-head {
    position: sticky;
    top: env(safe-area-inset-top, 0px);
    z-index: 30;
    margin: 0 -1.5rem;
    padding-top: 0.75rem;
    padding-right: 1.5rem;
    padding-left: 1.5rem;
    background: var(--color-slate-100);
  }
}

/* ---- Blocks ----------------------------------------------------------------
 *
 * Wider than a phone, a page is built from blocks: solid white pieces with a
 * clear edge, set on the ground with room between them. The head is one — the
 * name, where it is, and what can be done — and when a strip of tabs follows,
 * the strip is the foot of that same block rather than a line on the ground.
 * The explore bar, the table's sheet and a form's sheet are the blocks below
 * it. The chrome around them — the menu bar, the sidebar, the status bar — is
 * the ground itself, so nothing competes with the blocks for attention.
 *
 * Not on a phone: there the head is held at the top of the screen and reaches
 * both edges (above), and a block with a gutter either side is width a phone
 * cannot spare. `screen` so paper keeps the plain head. Not the sign-in card
 * or the front page, which draw their head their own way. */
@media screen and (min-width: 40.0625rem) {
  .page { padding: 2rem 2rem 4rem; }

  .page:not(.auth-page):not(.landing) > .page-head {
    padding: 1.25rem 1.5rem;
    border: 1px solid var(--color-slate-200);
    border-radius: var(--radius-xl);
    background: var(--color-white);
    box-shadow: var(--shadow-sm);
  }

  .page:not(.auth-page):not(.landing) > .page-head:has(+ .page-strip) {
    padding-bottom: 1rem;
    border-bottom-color: var(--color-slate-100);
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    box-shadow: none;
  }

  main .page > .page-head + .page-strip {
    padding: 0 0.75rem;
    border: 1px solid var(--color-slate-200);
    border-top: 0;
    border-radius: 0 0 var(--radius-xl) var(--radius-xl);
    background: var(--color-white);
    box-shadow: var(--shadow-sm);
  }

  main .page > .page-head + .page-strip a { padding-block: 0.625rem; }

  .page-body { padding-top: 1.5rem; }
}

/*
 * How-to pages. Short sentences, a numbered list, and a link to the thing
 * itself — the same weight as an empty-state explanation, not a manual.
 * `.not-prose` lifts the glossary out so its own layout is not restated.
 */
.guide-copy p + p,
.guide-copy p + ol,
.guide-copy ol + p,
.guide-copy .not-prose + p { margin-top: 1rem; }
.guide-copy p,
.guide-copy li { font-size: 0.9375rem; line-height: 1.6; color: var(--color-slate-700); }
.guide-copy ol {
  margin-top: 1rem;
  list-style: decimal;
  padding-left: 1.25rem;
}
.guide-copy li + li { margin-top: 0.75rem; }
.guide-copy li strong { color: var(--color-slate-900); font-weight: 600; }
.guide-copy a { color: var(--color-slate-900); text-decoration: underline; text-underline-offset: 0.12em; }
.guide-copy a:hover { color: var(--color-slate-600); }
.guide-copy em { font-style: italic; color: var(--color-slate-500); }

/*
 * A signed-out page — log in, sign up, reset a password — is the one place
 * nobody has a sidebar or a menu to anchor to yet, so it gets its own
 * centring rather than sitting flush with the top-left corner the way every
 * other `.page` does. `.page-head` stays for the "Log in" title, but the form
 * itself is lifted into a proper card with a shadow, the same weight the
 * floating surfaces elsewhere in the app already have.
 */
.auth-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: calc(100dvh - 3.2rem);
  padding: 1.5rem;
}

.auth-page .page-head {
  border-bottom: 0;
  padding-bottom: 0;
  text-align: center;
}

.auth-page .page-head .page-title { font-size: 1.75rem; }

.auth-card {
  width: 100%;
  max-width: 28rem;
  margin-top: 1.5rem;
  margin-inline: auto;
  padding: 1.75rem;
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
}

/*
 * `width: 100%` is the whole of why the card was never the width it asks for.
 *
 * `.auth-page` is a centred column, so every child of it is shrunk to fit
 * what is inside it — and `.page-body` is a plain block between the column
 * and the card. The card's own `width: 100%` then resolved against a box
 * that had already collapsed, and what decided the width in the end was the
 * *field's* intrinsic size: a text input is 20 characters wide when nothing
 * says otherwise. A 30 character email address arrived longer than the box,
 * and the person signing in read the end of their own address.
 *
 * So the block between them is told to be the page, and the card centres
 * itself inside it.
 */
.auth-page .page-body { padding-top: 0; width: 100%; }

/*
 * On a phone the card's own padding is the only thing between the field and
 * the edge of the screen, and 1.75rem of it on each side is 56px that the
 * email address could have had. Narrower here, and nowhere else: on a screen
 * with room the padding is what makes the card a card.
 */
@media (max-width: 26rem) {
  .auth-page { padding: 1rem; }
  .auth-card { padding: 1.25rem; }
}

/*
 * A field on this card is drawn larger than a field anywhere else, and the
 * helper that draws every other field is deliberately untouched. `h-9` and
 * `text-sm` are right in a table and right in a dense form, where a field is
 * one control among many. Here the form is the whole page: three fields, and
 * nothing else to read, on the one page somebody meets before they have
 * an account.
 *
 * 16px is a number rather than a taste. Below it, iOS Safari zooms the page
 * when the field takes focus, and the first thing a new account does is push
 * the form off the side of the screen.
 *
 * 2.5rem is the `lg` height in `ButtonsHelper`, which is the size the submit
 * button on each of these pages now asks for, so the card is one size rather
 * than two.
 */
.auth-card input[type="email"],
.auth-card input[type="password"],
.auth-card input[type="text"] {
  height: 2.5rem;
  font-size: 1rem;
  padding-inline: 0.75rem;
}

/* ---- The front page ---------------------------------------------------------
 *
 * The same four regions as every other page — `page-head` still says what this
 * is and what you can do about it — drawn larger, because this one is read by
 * somebody who has not decided to be here yet.
 *
 * Qualified by `.landing` rather than written as utilities on the elements:
 * `.page-title` and `.page-head__note` are declared in this file, so a utility
 * on the tag would lose to them. That is the rule in docs/design.md, applied
 * rather than worked around.
 */
.landing .page-head {
  border-bottom: 0;
  padding-top: 2.5rem;
  padding-bottom: 0;
}

.landing .page-title {
  flex-basis: 100%;
  max-width: 20ch;
  font-size: clamp(2rem, 5vw, 3rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
}

.landing__promise {
  max-width: 46ch;
  margin-top: 0.875rem;
  font-size: 1.0625rem;
  line-height: 1.6;
}

/* Its own line, rather than beside the promise. `.page-head` lays out with
   `space-between`, so left to itself the pair of buttons sits out to the right
   of a paragraph and level with its first line — which reads as two unrelated
   things on one row rather than as the sentence and what to do about it. */
.landing .page-actions {
  flex-basis: 100%;
  margin-top: 1.75rem;
}

.landing .page-body { padding-top: 3rem; }

/* Four of them, so two by two rather than `auto-fit` — which fits three across
   at this width and leaves the fourth alone on a row of its own. */
.landing__claims {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 48rem) {
  .landing__claims { grid-template-columns: 1fr 1fr; }
}

/* Same tokens as `.landing__panel`, so the four claims and the pair of
   drawings stay one surface. Named here because the markup used to write
   the same utility string four times. */
.landing__claim {
  margin: 0;
  padding: 1.25rem;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  box-shadow: var(--shadow-sm);
}

.landing__claim-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-slate-900);
}

.landing__claim-body {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--color-slate-600);
}

.landing__words { margin-top: 3rem; }

/* Two panels, side by side where there is room and stacked where there is not
   — the whole point is reading them against each other, so they never sit at
   different widths. */
.landing__pair {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  margin-top: 1.25rem;
}

/* Not a screenshot: a drawing of one, out of the same tokens the real page is
   drawn from, so it turns over with the theme and cannot go stale the way an
   image of a page from six months ago does. */
.landing__panel {
  margin: 0;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.landing__panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--color-slate-200);
}

.landing__panel-title {
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--color-slate-900);
}

.landing__panel-action {
  border-radius: var(--radius-md);
  padding: 0.25rem 0.625rem;
  background: var(--color-accent);
  color: var(--color-accent-ink);
  font-size: 0.75rem;
  font-weight: 500;
}

.landing__panel-body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
}

/* Stand-ins for writing, because what the pair is about is the words in the
   furniture rather than anything in the middle. Three different lengths so it
   reads as a list of things rather than as a loading state. */
.landing__panel-line {
  height: 0.5rem;
  border-radius: 999px;
  background: var(--color-slate-200);
}
.landing__panel-line:nth-child(2) { width: 75%; }
.landing__panel-line:nth-child(3) { width: 55%; }

.landing__panel-foot {
  padding: 0.625rem 1rem;
  border-top: 1px solid var(--color-slate-200);
  background: var(--color-slate-50);
  font-size: 0.75rem;
  color: var(--color-slate-600);
}

.landing__close {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  margin-top: 3rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--color-slate-200);
}

/* Arrangements — where the cards go. See Layout. */

.stack { display: flex; flex-direction: column; gap: 0.5rem; }

/* ---- The same rows, under headings -------------------------------------
 *
 * A list told what to group by draws a section per value. <details> because
 * collapsing is a disclosure and the browser already has one — no controller,
 * no request, and it keeps working with no JavaScript at all.
 *
 * The heading is sticky inside its own group, so scrolling a long one still
 * says which group you are in. */

/* ---- The row under the pointer -------------------------------------------
 *
 * A row lights up as you pass over it, which a table of fifty rows needs to
 * keep your eye on one line across twelve columns.
 *
 * Painted as a `background-image` rather than a `background-color`, because a
 * row may already be tinted by one of its view's colour rules and a colour
 * would replace that tint instead of sitting on top of it. An image layers
 * over the colour underneath, so an amber row stays amber and still lifts.
 *
 * `[data-narrow-row]` is the real rows only. The blank row at the foot is
 * where the next one is typed, and it is already tinted for that. */

@media (hover: hover) {
  tbody tr[data-narrow-row]:hover {
    background-image: linear-gradient(
      color-mix(in srgb, var(--color-slate-900) 4%, transparent),
      color-mix(in srgb, var(--color-slate-900) 4%, transparent));
  }
}

/* What you can do to a row is worth less of the page than what the row says.
 * Edit and Open used to sit at the end of every line at full strength, so
 * fifty rows drew a column of a hundred links nobody had asked for.
 *
 * `opacity` rather than `display`, so the actions keep their space and the
 * rows do not shuffle when the pointer arrives. They stay reachable by
 * keyboard throughout — `:focus-within` brings them back, and an element at
 * zero opacity is still in the document for a screen reader.
 *
 * Wrapped in `@media (hover: hover)` on purpose: a touch screen has no hover
 * to reveal anything with, so there the actions are simply always visible.
 * Same reasoning as the hints above. */

@media (hover: hover) {
  tbody tr[data-narrow-row] [data-inline-edit-target="displayActions"] {
    opacity: 0;
    transition: opacity 100ms ease-out;
  }

  tbody tr[data-narrow-row]:hover [data-inline-edit-target="displayActions"],
  tbody tr[data-narrow-row]:focus-within [data-inline-edit-target="displayActions"] {
    opacity: 1;
  }
}

/* ---- The cell the keyboard is on ----------------------------------------
 *
 * One ring, the same accent every other control uses. An inset box-shadow
 * rather than an outline: a table cell clips `outline`, and the rows sit
 * in a frame that scrolls sideways. Drawn from `data-grid-focus`, not
 * `:focus-visible`, so the ring can sit on a cell after a save morph
 * without stealing the cursor. While a cell is open the mark is lifted and
 * the input keeps the ordinary focus ring. */

[data-controller="cell-edit"] td[data-inline-edit-field]:focus,
[data-controller="cell-edit"] td[data-inline-edit-field]:focus-visible {
  outline: none;
}

[data-controller="cell-edit"] td[data-grid-focus],
[data-controller="cell-edit"] td[data-grid-focus]:focus,
[data-controller="cell-edit"] td[data-grid-focus]:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--color-accent);
}

/* The mark waits while the cursor is somewhere else. A page load puts the
 * cursor in the blank row's first field, and a ring on a cell at the same
 * time is two marks each saying "you are here". So while a field anywhere
 * on the page has the keyboard's focus, the cell draws no ring — it cannot
 * have the focus itself then, since only one thing can. It keeps its place
 * in the tab order, and the ring comes back the moment the field is left.
 * After a save morph nothing has focus, so the mark still sits where it was
 * without stealing the cursor. `:focus-visible`, not `:focus`, like every
 * rule here: a text field matches it however it was focused. */
body:has(:is(input, select, textarea, [contenteditable="true"]):focus-visible)
  [data-controller="cell-edit"] td[data-grid-focus] {
  box-shadow: none;
}
body:has(:is(input, select, textarea, [contenteditable="true"]):focus-visible)
  [data-controller="cell-edit"] td[data-grid-focus][data-stuck-end] {
  box-shadow: 1px 0 0 var(--color-slate-200);
}

/* ---- A range of cells, and what it comes to -----------------------------
 *
 * Shift-arrow or shift-click marks a block of cells; the bar under the table
 * says its count, and its sum when the block is a single number column. A soft
 * accent fill — the same token the accent-soft ground uses — so it reads in
 * both schemes, and the ring on the moving end still sits over it. */

[data-controller="cell-edit"] td[data-grid-selected] {
  background: var(--color-accent-soft);
}

.selection-summary {
  margin-top: 0.5rem;
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
  color: var(--color-slate-600);
}

/* ---- Sticky leading columns ----------------------------------------------
 *
 * The leading `th`/`td` stay in view while the rest of a wide table scrolls.
 * Offsets are measured into `--stick-left` by sticky_columns_controller,
 * because a title column is not the same width as a fee. The fill is
 * opaque so scrolling cells do not show through; a tinted row and the
 * reading mark keep their own faces. A 1px edge on the last pinned cell is
 * how you see the freeze, not a second scroller. See docs/sticky-columns.md.
 */
[data-stuck] {
  position: sticky;
  left: var(--stick-left, 0px);
  z-index: 1;
  background-color: var(--color-white);
}

/* Sticky table cells in Chrome skip `display: none` on their descendants,
 * so a closed cell's input would show through the value. The class is still
 * Tailwind's `hidden`; this holds it on every cell editor, pinned or not. */
[data-inline-edit-target].hidden { display: none !important; }
thead [data-stuck] { z-index: 3; }
tfoot [data-stuck] { z-index: 2; }
[data-stuck-end] { box-shadow: 1px 0 0 var(--color-slate-200); }
[data-controller="cell-edit"] td[data-grid-focus][data-stuck-end] {
  box-shadow: inset 0 0 0 2px var(--color-accent), 1px 0 0 var(--color-slate-200);
}

[data-controller="sticky-columns"] { position: relative; }

tr.bg-slate-50 [data-stuck] { background-color: var(--color-slate-50); }
tr.bg-blue-50 [data-stuck] { background-color: var(--color-blue-50); }
tr.bg-green-50 [data-stuck] { background-color: var(--color-green-50); }
tr.bg-amber-50 [data-stuck] { background-color: var(--color-amber-50); }
tr.bg-red-50 [data-stuck] { background-color: var(--color-red-50); }
tr.bg-purple-50 [data-stuck] { background-color: var(--color-purple-50); }
tr[data-reading] [data-stuck] { background-color: var(--color-slate-50); }
[data-stuck][data-grid-selected] { background-color: var(--color-accent-soft); }

/* ---- Fill-down ------------------------------------------------------------
 *
 * A small accent square on the corner of a closed cell. Dragging it down
 * (or up) copies that cell's value through the ordinary batch set. It sits
 * inside the table's own scroller and is positioned from the cell's box, so
 * a morph never finds it inside a row. */
.fill-handle {
  position: absolute;
  width: 8px;
  height: 8px;
  padding: 0;
  border: 1px solid var(--color-white);
  background: var(--color-accent);
  cursor: crosshair;
  z-index: 5;
}

.fill-handle:hover { background: var(--color-accent-strong); }

/* ---- What a column comes to ---------------------------------------------
 *
 * Under the rows rather than over them, and quieter than they are: a total is
 * read after the column, not instead of it. */

.table-foot {
  border-top: 2px solid var(--color-slate-200);
  background: var(--color-slate-50);
  font-size: 0.8125rem;
}

.table-foot__label {
  display: block;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-slate-500);
}

.table-foot__value { font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- Table density -------------------------------------------------------
 *
 * How tight the density setting draws the rows. On the frame, so it reaches
 * every cell — the head, the body and the foot. Comfortable is the default
 * and adds no class, so there is nothing to undo: it is the cells' own
 * `py-3` (0.75rem). Compact fits more of a long table on one screen; spacious
 * opens the rows for reading and gives a finger more to land on. A `td`/`th`
 * selector clears the per-cell padding utility the cells carry, which a bare
 * class could not. See View#density and ViewsHelper#table_density_class. */

.is-dense td, .is-dense th { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.is-roomy td, .is-roomy th { padding-top: 1.125rem; padding-bottom: 1.125rem; }

.grouping__total {
  margin-left: 0.5rem;
  font-size: 0.75rem;
  color: var(--color-slate-500);
  font-variant-numeric: tabular-nums;
}

/* Named `grouping` rather than `group`: Tailwind gives `group` its own
   meaning — the parent half of `group-hover` — and the sidebar already uses
   it. Two things called the same thing in one page is one too many. */
.grouping {
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  background: var(--color-white);
}

.grouping__head {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-radius: 0.5rem;
  background: var(--color-white);
  border-bottom: 1px solid transparent;
}

.grouping[open] .grouping__head {
  border-bottom-color: var(--color-slate-200);
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

.grouping__label { font-size: 0.875rem; font-weight: 500; color: var(--color-slate-900); }

.grouping__body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
}

/* The box that makes a row in this group. Dashed rather than a row, because it
   is an invitation and not a row yet — the same reasoning the board's column
   composer follows. */
.grouping__compose {
  padding: 0.5rem 0.625rem;
  border: 1px dashed var(--color-slate-300);
  border-radius: 0.5rem;
  background: var(--color-white);
}

.grouping__compose-actions {
  display: block;
  margin-top: 0.375rem;
  text-align: right;
  font-size: 0.75rem;
}

.grouping__compose-errors {
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--color-red-800);
}

/* A folder in the sidebar: a heading its lists fold under. Native <details>,
   the same collapsing the grouped list uses, so the fold is the element rather
   than script. `folder` and not `group`, for the reason `.grouping` gives: the
   sidebar already means `group` as Tailwind's hover parent. The colour lives on
   the summary as utilities; only the marker and the chevron turn are here. */
.folder__head {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  cursor: pointer;
  list-style: none;
}

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

.folder__chevron { transition: transform 150ms ease; }

.folder[open] > .folder__head .folder__chevron { transform: rotate(90deg); }

/* The stack of inputs a quick-add composer shows — a label over each field, on
   a board column and at the foot of a group alike. One column so it reads the
   same in a narrow board column as in a wide list. */
.composer-fields {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.composer-field {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.composer-field__label {
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--color-slate-500);
}

/* A composer on a board column or under a group asks for its title and
   nothing else until it has focus. Each one asks for every field a card shows,
   and one sits under every column, so on a board of short columns the forms
   were taller than the cards. Typing the name opens the rest; clicking away
   folds it again and keeps what was typed, because a field that is not
   displayed still holds its value and is still sent. `:focus-within`, so there
   is no script and nothing different offline — a <select> or a date picker
   that is open keeps focus, so it does not fold under the pointer.

   Two things never fold: a composer with no title field (it would fold to
   nothing), and a refused one (`data-composer-held`), which must show the
   field its error is about. The table's blank row does not fold at all — it
   shows every field on purpose; see items/_new_row.
   docs/what-the-screens-say.md §4. */
.composer-fold:not(:focus-within):not([data-composer-held]):has(.composer-field--title)
  .composer-field:not(.composer-field--title) {
  display: none;
}

/* The table's own composer: a wrapping grid so every field the list
   asks for stays on screen, rather than one cell per column disappearing
   off the right-hand edge. A board column stays a stack — it is already
   one field wide. */
.composer-fields--wide {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.5rem 0.75rem;
}

.composer-fields--wide .composer-field { min-width: 0; }

.composer-fields--wide .composer-field:has(textarea) {
  grid-column: 1 / -1;
}

.table-compose {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border: 1px dashed var(--color-slate-300);
  border-radius: 0.5rem;
  background: var(--color-white);
}

.table-compose__lead {
  margin: 0 0 0.5rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-slate-700);
}

.table-compose__actions {
  display: block;
  margin-top: 0.5rem;
  text-align: right;
  font-size: 0.75rem;
}

.table-compose__errors {
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--color-red-800);
}

/* The gallery arrangement: cards side by side, as many as the width holds.
   Its own name, never `.grid` — that is Tailwind's utility too, and a rule
   here sits outside Tailwind's layers, so it beat every `grid-cols-*` in the
   app. test/theme_test.rb refuses the collision. */
.gallery-grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}

.board {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  padding-bottom: 0.5rem;
  align-items: flex-start;
}

/* A two-axis board: lanes stacked down the page, each its own row of the same
   columns. The columns are fixed width (.board-column below), so they line up
   from one lane to the next. Each lane scrolls its own columns; keeping the
   lanes' horizontal scroll in step is a later refinement. See
   items/arrangements/_columns and docs/board.md. */
.board-grid {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.board-lane__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.375rem;
  padding-inline-start: 0.125rem;
}

.board-column {
  flex: 0 0 17rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  background: var(--color-slate-100);
}

.board-column__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.625rem 0.75rem;
  border-bottom: 1px solid var(--color-slate-200);
}

/* The grip a card is dragged by. Quiet until the pointer is on the card, so
   a board at rest is cards rather than furniture. */
.card__handle {
  cursor: grab;
  color: var(--color-slate-300);
  font-size: 0.875rem;
  line-height: 1;
}

.card:hover .card__handle { color: var(--color-slate-500); }

.board-column__body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  max-height: 60vh;
  overflow-y: auto;
}

/* The box that makes a card in this column, kept under the scrollable
   list so it stays in reach when the column is long. Dashed rather than
   a card, because it is an invitation and not a row yet. */
.board-column__compose {
  margin: 0 0.75rem 0.75rem;
  padding: 0.5rem 0.625rem;
  border: 1px dashed var(--color-slate-300);
  border-radius: 0.5rem;
  background: var(--color-white);
}

.board-column__compose-actions {
  display: block;
  margin-top: 0.375rem;
  text-align: right;
  font-size: 0.75rem;
}

.board-column__compose-errors {
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--color-red-800);
}

.calendar { border: 1px solid var(--color-slate-200); border-radius: 0.5rem; overflow: hidden; }

.calendar__head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  padding: 0.625rem;
  border-bottom: 1px solid var(--color-slate-200);
  background: var(--color-slate-100);
}

.calendar__month { margin: 0; font-size: 0.9375rem; font-weight: 600; }
.calendar__month a { color: inherit; text-decoration: none; }

.calendar__step {
  color: var(--color-slate-600);
  text-decoration: none;
  padding: 0 0.5rem;
  border-radius: 0.25rem;
}
.calendar__step:hover { color: var(--color-slate-900); background: var(--color-slate-200); }

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}

.calendar__dayname {
  padding: 0.375rem 0.5rem;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-slate-600);
  border-bottom: 1px solid var(--color-slate-200);
}

.calendar__cell {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-height: 6.5rem;
  padding: 0.375rem;
  border-top: 1px solid var(--color-slate-200);
  border-left: 1px solid var(--color-slate-200);
  overflow-y: auto;
}
.calendar__cell:nth-child(7n + 1) { border-left: 0; }
.calendar__cell--outside { background: var(--color-slate-100); }
.calendar__cell--outside .calendar__number { color: var(--color-slate-400); }
.calendar__cell--today .calendar__number {
  background: var(--color-slate-900);
  color: var(--color-white);
  border-radius: 999px;
}

.calendar__number {
  align-self: flex-start;
  min-width: 1.25rem;
  padding: 0 0.25rem;
  font-size: 0.6875rem;
  text-align: center;
  color: var(--color-slate-600);
}

/* A chip that started on an earlier day of this range. The card itself sits
   on the first visible day, so this is the rest of the span, not a second
   copy of the row. */
.calendar__continuation {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
  background: var(--color-slate-200);
  color: var(--color-slate-700);
  font-size: 0.75rem;
}

/* A spanning chip's edges, the same gesture a timeline bar already
   owns. See timeline_drag_controller and docs/calendars.md. */
.calendar-chip { position: relative; }
.calendar-chip:not(.calendar__continuation):not(:has(.card)) { display: none; }
.calendar-chip__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 0.75rem;
  cursor: ew-resize;
  touch-action: none;
  background: var(--color-accent);
  opacity: 0.35;
}
.calendar-chip__handle[data-edge="start"] { left: 0; }
.calendar-chip__handle[data-edge="end"] { right: 0; }
.calendar__cell--draft {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* The whole date, for the phone rules below. Drawn always and hidden here,
   because a grid cell has room for a number and nothing else. */
.calendar__date { display: none; }

.calendar__nothing { display: none; }

/*
 * ---- A month on a phone -------------------------------------------------
 *
 * Seven columns across 390px is about 50px a day, and a card in 50px is a
 * word broken over three lines beside its own horizontal scrollbar. It was
 * unreadable, and it was the only arrangement that was: a board scrolls its
 * columns sideways in its own box and reads fine, a gallery is already one
 * column, and a dashboard already stacks.
 *
 * So a month becomes an agenda — the days that have something on them, in
 * order, each with its rows under it. That is what every calendar on a phone
 * does, and it is the same trick `.table-stack` uses further down: **the same
 * markup restacked, never a second rendering.** The server draws one month
 * and does not know or care which of the two it is about to be.
 *
 * `:has()` is what makes it possible to keep it in CSS. An empty Tuesday is
 * part of what a grid says and is nothing at all in a list, so the cells with
 * no card in them go — and if that leaves no cells at all, the same selector
 * is what brings the sentence saying so back.
 */
@media (max-width: 40rem) {
  .calendar__grid { display: block; }

  /* An agenda has no columns, so it has nothing to head. */
  .calendar__dayname { display: none; }

  /* A day with nothing on it is a day an agenda does not mention. */
  .calendar__cell:not(:has(.card)):not(:has(.calendar__continuation)) { display: none; }

  .calendar__cell {
    min-height: 0;
    padding: 0.625rem 0.75rem;
    border-left: 0;
    overflow-y: visible;
  }

  /* A day borrowed from the month either side is still a day with something
     on it. Greying it says "outside the grid", and there is no grid. */
  .calendar__cell--outside { background: transparent; }

  /* The number was the whole label because the column above it said which
     weekday it was. Nothing says that now, so the date does. */
  .calendar__number { display: none; }

  .calendar__date {
    display: block;
    align-self: flex-start;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-slate-500);
  }

  .calendar__cell--today .calendar__date { color: var(--color-accent); }

  /* A month with nothing in it draws no cells at all, which reads as a page
     that failed rather than as a quiet month. */
  .calendar__grid:not(:has(.card)):not(:has(.calendar__continuation)) + .calendar__nothing {
    display: block;
    padding: 1.25rem 0.75rem;
    color: var(--color-slate-500);
  }
}

/*
 * ---- An agenda by choice ------------------------------------------------
 *
 * The month grid restacked as a list — the days that have something on them,
 * in order — which is exactly what the phone rule above forces. The `agenda`
 * grain offers that same look at any width: the same markup and the same
 * `:has()` tricks, a class choosing it rather than the screen. The bodies
 * repeat the media query's on purpose; CSS has no way to share a rule between
 * a width and a class, and a preprocessor is a second language this project
 * does not run. See View#calendar_grain and items/arrangements/_cells.
 */
.calendar--agenda .calendar__grid { display: block; }
.calendar--agenda .calendar__dayname { display: none; }
.calendar--agenda .calendar__cell:not(:has(.card)):not(:has(.calendar__continuation)) { display: none; }

.calendar--agenda .calendar__cell {
  min-height: 0;
  padding: 0.625rem 0.75rem;
  border-left: 0;
  overflow-y: visible;
}

.calendar--agenda .calendar__cell--outside { background: transparent; }
.calendar--agenda .calendar__number { display: none; }

.calendar--agenda .calendar__date {
  display: block;
  align-self: flex-start;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-slate-500);
}

.calendar--agenda .calendar__cell--today .calendar__date { color: var(--color-accent); }

.calendar--agenda .calendar__grid:not(:has(.card)):not(:has(.calendar__continuation)) + .calendar__nothing {
  display: block;
  padding: 1.25rem 0.75rem;
  color: var(--color-slate-500);
}

/* A jump back to today, beside the step arrows on a week or day calendar. */
.calendar__today {
  font-size: 0.75rem;
  color: var(--color-slate-600);
  text-decoration: none;
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.25rem;
}
.calendar__today:hover { color: var(--color-slate-900); background: var(--color-slate-200); }

/*
 * ---- A week or a day, down an hour axis ---------------------------------
 *
 * The calendar at the week or day grain. When the field carries a time the
 * grid is a label column and a column per day, with a row per hour, so a
 * meeting sits at the height its clock says — the time of day a month cell has
 * nowhere to show. When the field is a plain date there are no hours, so it is
 * a strip of the same day cells the month draws. See _calendar_schedule.
 */
.schedule { display: grid; }

/* Timed: an hour label column, then one column per day. The whole thing
   scrolls in its own box so an early hour and a late one are both reachable
   without the page growing a day tall. */
.schedule--timed {
  max-height: 32rem;
  overflow-y: auto;
  border-top: 1px solid var(--color-slate-200);
}
.schedule--timed.schedule--w7 { grid-template-columns: 3.5rem repeat(7, minmax(0, 1fr)); }
.schedule--timed.schedule--w1 { grid-template-columns: 3.5rem minmax(0, 1fr); }

/* Strip: a plain date has no time, so the days are the month's own cells,
   narrowed to the run on screen. */
.schedule--strip.schedule--w7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.schedule--strip.schedule--w1 { grid-template-columns: minmax(0, 1fr); }

.schedule__corner { position: sticky; top: 0; background: var(--color-slate-100); z-index: 1; }

.schedule__dayhead {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 0.375rem 0.5rem;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-slate-600);
  background: var(--color-slate-100);
  border-bottom: 1px solid var(--color-slate-200);
  border-left: 1px solid var(--color-slate-200);
}
.schedule__dayhead--today { color: var(--color-accent); }

.schedule__hour {
  padding: 0.25rem 0.5rem;
  font-size: 0.6875rem;
  color: var(--color-slate-500);
  text-align: right;
  border-top: 1px solid var(--color-slate-200);
}

.schedule__slot {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-height: 2.25rem;
  padding: 0.25rem;
  border-top: 1px solid var(--color-slate-200);
  border-left: 1px solid var(--color-slate-200);
}
.schedule__slot--now { background: var(--color-slate-100); }

.schedule__loose { padding: 0.625rem 0.75rem; border-top: 1px solid var(--color-slate-200); }
.schedule__loose-head {
  margin: 0 0 0.375rem;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-slate-500);
}

/* Shared small pieces the arrangements lean on. */

.card {
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  padding: 0.75rem 1rem;
  box-shadow: var(--shadow-sm);
  transition: box-shadow 150ms ease-out, border-color 150ms ease-out;
}
.card:hover {
  border-color: var(--color-slate-400);
  box-shadow: var(--shadow-md);
}
/* A row that changed since you last opened this view wears an amber stripe
   down its leading edge — the row-level companion to the "changed since you
   last looked" count. Added by the `since` controller, off each row's own
   updated_at against the instant of your last visit. The card and list-row
   keep their own elevation by drawing the stripe alongside it. See
   since_controller.js and docs/changed-since.md. */
tr.is-new-since > td:first-child { box-shadow: inset 3px 0 0 var(--color-amber-500); }
.card.is-new-since { box-shadow: inset 3px 0 0 var(--color-amber-500), var(--shadow-sm); }
.card.is-new-since:hover { box-shadow: inset 3px 0 0 var(--color-amber-500), var(--shadow-md); }
.list-row.is-new-since { box-shadow: inset 3px 0 0 var(--color-amber-500); }

.card--compact { padding: 0.25rem 0.375rem; font-size: 0.75rem; border-radius: 0.25rem; }
.card--compact .footer, .card--compact .bodytext { display: none; }

/* ---- The list arrangement -----------------------------------------------
 *
 * One line an item: denser than the card stack, lighter than the table. A
 * bordered panel with divided rows on its own; inside a group's body the
 * heading provides the border and the rows just divide. The same accent tint a
 * card and a table row take, read off the row's own values. */

.list {
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  background: var(--color-white);
  overflow: hidden;
}

.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  border-top: 1px solid var(--color-slate-100);
}
.list-row:first-child { border-top: 0; }
.list-row:hover { background: var(--color-slate-50); }
.list-row--child { padding-inline-start: 2rem; }

.list-row__main {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.75rem;
}
.list-row__title { font-weight: 500; color: var(--color-slate-900); }
.list-row__title:hover { text-decoration: underline; }

.list-row__fields {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.75rem;
  font-size: 0.8125rem;
  color: var(--color-slate-500);
}
.list-row__label { margin-inline-end: 0.25rem; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.6875rem; color: var(--color-slate-400); }

.list-row__actions {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.5rem;
}
.list-row__action { font-size: 0.75rem; color: var(--color-slate-400); }
.list-row__action:hover { color: var(--color-slate-900); }

/* A file field that holds a picture draws one. The size is the page's
   business rather than the partial's: one cell partial is a table row, a card
   slot and a row page, and each wants a different amount of room. One variant
   serves all three — see Thumbnail — so this is CSS rather than a second
   transformation. */
.thumb {
  height: 2rem;
  width: 2rem;
  border-radius: 0.25rem;
  object-fit: cover;
  background: var(--color-slate-100);
}
.card .thumb { height: 5rem; width: 5rem; }
.card--compact .thumb { height: 1.5rem; width: 1.5rem; }
/* The row page has room, and it is where somebody goes to look at the thing
   rather than to scan a list of them. */
dd .thumb { height: 8rem; width: 8rem; }

/* A row drawn under another one. The indent and the mark are the whole of
   what nesting looks like, and they are CSS because the row itself is the
   same row wherever it is drawn — see Nesting. */
.row-child {
  padding-left: 2.5rem;
  position: relative;
}
.row-child::before {
  content: "\21B3";
  position: absolute;
  left: 1.25rem;
  color: var(--color-slate-400);
}
/* A card under another card. Indented rather than marked, because a card is
   already a box and a second mark inside it reads as content. */
.card--child { margin-left: 2rem; }

/* A card left sitting. Untouched for longer than the view's threshold, it
 * fades to say so at a glance — chrome off `updated_at`, never a mark on the
 * data. Pointing at it brings it back to full, so a faded card is still a card
 * to read and act on, not one held at arm's length. See View#stale? and
 * docs/card-age.md. */
.card.is-stale,
.list-row.is-stale {
  opacity: 0.55;
  transition: opacity 120ms ease-out;
}
.card.is-stale:hover,
.card.is-stale:focus-within,
.list-row.is-stale:hover,
.list-row.is-stale:focus-within {
  opacity: 1;
}

/* The row a drop is about to nest under, while a drag hovers its middle. Held
   only during the drag by sortable_controller — it says "under this one"
   before the mouse is let go, which is what keeps a drop-to-nest from looking
   the same as a drop-to-reorder. An outline rather than a fill, so it reads on
   a row that already carries a colour rule's tint. */
.nest-target > td { box-shadow: inset 0 0 0 2px var(--color-accent); }
.nest-target > td:first-child { box-shadow: inset 2px 0 0 0 var(--color-accent), inset 0 2px 0 0 var(--color-accent), inset 0 -2px 0 0 var(--color-accent); }
.nest-target > td:last-child { box-shadow: inset -2px 0 0 0 var(--color-accent), inset 0 2px 0 0 var(--color-accent), inset 0 -2px 0 0 var(--color-accent); }

.chip {
  display: inline-block;
  border-radius: 999px;
  padding: 0.0625rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 500;
}
.chip--empty { background: var(--color-slate-200); color: var(--color-slate-600); }

.count { font-size: 0.75rem; color: var(--color-slate-600); font-variant-numeric: tabular-nums; }
/* A board column over its work-in-progress limit. Colour only — the count is
   still the truth, so the number reads clearly rather than being hidden. */
.count--over { color: var(--color-red-700); font-weight: 600; }

/* A type's mark, on the same gradient as the brand mark — one small
   splash of colour on a tile that would otherwise be all text, and a
   way to tell two tiles apart before you've read either name. */
.type-badge {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-md);
  /* A fill, so it is painted in the fill colours rather than in two rungs of
     ramps that turn over in the dark — see the tailwind theme. */
  background: linear-gradient(135deg, var(--color-accent), var(--color-brand-warm));
  color: var(--color-on-fill);
  font-weight: 700;
  font-size: 0.9375rem;
}

/* A page with nothing on it yet is not a failed page — it is drawn as its
   own quiet placeholder rather than a stray line of text, so a brand-new
   tracker's first screen looks considered rather than broken. */
.empty {
  color: var(--color-slate-600);
  padding: 2.5rem 1.5rem;
  text-align: center;
  border: 1px dashed var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-slate-100);
}
.empty a { font-weight: 500; }

/* The slim variant sits inside furniture that already has its own border
   (a board column, an archive row) — it stays a plain line, not a second box
   nested inside the first one. */
.empty--slim {
  padding: 0.25rem 0;
  font-size: 0.75rem;
  text-align: left;
  border: 0;
  background: transparent;
}

/* A blank state drawn as a surface: the `.empty` box with a mark above the
 * words. The mark sits on a clean disc so it reads as a designed element, not
 * a stray glyph; the disc is the card face, the glyph a quiet slate. See
 * application/_blank_slate and docs/design-next.md item C. */
.blank-slate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--color-slate-600);
  border: 1px dashed var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-slate-100);
}
.blank-slate__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 9999px;
  color: var(--color-slate-400);
  background: var(--color-white);
}
.blank-slate__body { max-width: 24rem; font-size: 0.875rem; }
.blank-slate a { font-weight: 500; }

/* The slim variant, for furniture that already has its own border and its own
 * actions (a related list): the mark sits beside the line, no box, the same
 * reason `.empty--slim` drops the box. */
.blank-slate--slim {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  padding: 0.5rem 0;
  text-align: left;
  border: 0;
  background: transparent;
}
.blank-slate--slim .blank-slate__mark {
  width: 1.75rem;
  height: 1.75rem;
  background: transparent;
}
.blank-slate--slim .blank-slate__body {
  font-size: 0.8125rem;
  color: var(--color-slate-500);
}

/*
 * The confirmation Turbo asks through. See app/javascript/confirm.js.
 *
 * Sized and worded like part of the app rather than part of the browser,
 * because what it is asking — do you know what this takes with it — is a
 * question only the app can pose.
 */
.confirm {
  /* Centred like any modal. The base styles set every margin to 0, and a
     dialog centres itself only through `margin: auto` — without this line the
     delete confirmation and the outbox review opened in the top-left corner. */
  margin: auto;
  max-width: 26rem;
  padding: 1.25rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.625rem;
  color: var(--color-slate-900);
  box-shadow: var(--shadow-lg);
}

.confirm::backdrop { background: color-mix(in srgb, var(--color-slate-950) 35%, transparent); }

.confirm__message { margin: 0 0 1.25rem; font-size: 0.9375rem; line-height: 1.5; }

.confirm__actions { display: flex; justify-content: flex-end; gap: 0.5rem; }

.confirm__cancel,
.confirm__go {
  border-radius: 0.375rem;
  padding: 0.5rem 0.875rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
}

.confirm__cancel { border: 1px solid var(--color-slate-200); background: var(--color-white); color: var(--color-slate-900); }
.confirm__cancel:hover { border-color: var(--color-slate-600); }

/* The destructive one is not the quiet one, but it is not the default either:
   the dialog opens with nothing focused rather than with "yes" under a
   thumb. */
.confirm__go { border: 0; background: var(--color-danger); color: var(--color-on-fill); }
.confirm__go:hover { background: var(--color-danger-strong); }

/*
 * The list of writes the server refused. See sign_out_controller.js's
 * neighbour, outbox_review_controller.js — same dialog element, `.confirm`
 * for the box itself, but a list rather than a single message.
 */
#outbox-review { max-width: min(40rem, calc(100vw - 2rem)); }

/*
 * The jump box. See app/javascript/controllers/palette_controller.js and
 * app/models/palette.rb.
 *
 * A command box sits high rather than centred: the eye is already at the top
 * of the page reaching for it, and a list that grows downward from there does
 * not walk under the hand that opened it. Otherwise it is the confirm dialog's
 * materials — the same border, radius, shadow and backdrop — because it is the
 * same kind of thing, a panel the app raises over what you were doing.
 */
.palette {
  width: min(40rem, calc(100vw - 2rem));
  max-width: none;
  margin: 10vh auto auto;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.75rem;
  background: var(--color-white);
  color: var(--color-slate-900);
  box-shadow: 0 20px 50px color-mix(in srgb, var(--color-slate-950) 25%, transparent);
}

.palette::backdrop { background: color-mix(in srgb, var(--color-slate-950) 35%, transparent); }

/* The shortcuts card — the jump box's quieter sibling, opened with ?. Same
 * white panel over the same dark, narrower because it is a list to read rather
 * than a box to type in. See shortcuts_controller.js. */
.shortcuts {
  width: min(30rem, calc(100vw - 2rem));
  max-width: none;
  margin: 10vh auto auto;
  padding: 0;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.75rem;
  background: var(--color-white);
  color: var(--color-slate-900);
  box-shadow: 0 20px 50px color-mix(in srgb, var(--color-slate-950) 25%, transparent);
}
.shortcuts::backdrop { background: color-mix(in srgb, var(--color-slate-950) 35%, transparent); }
.shortcuts__card { padding: 1.25rem 1.5rem 1.5rem; }
.shortcuts__head { display: flex; align-items: center; justify-content: space-between; }
.shortcuts__title { font-size: 0.95rem; font-weight: 600; }
.shortcuts__close {
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--color-slate-400);
  line-height: 1;
}
.shortcuts__close:hover { color: var(--color-slate-700); }
.shortcuts__group {
  margin: 1.25rem 0 0.25rem;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-slate-400);
}
.shortcuts__list { margin: 0.5rem 0 0; }
.shortcuts__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.35rem 0;
  font-size: 0.85rem;
  color: var(--color-slate-700);
}
.shortcuts__row dd { margin: 0; white-space: nowrap; color: var(--color-slate-500); }
.shortcuts__or { color: var(--color-slate-300); }
.shortcuts kbd {
  display: inline-block;
  min-width: 1.3em;
  padding: 0.1rem 0.35rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.3rem;
  background: var(--color-slate-50);
  font-size: 0.75rem;
  text-align: center;
  color: var(--color-slate-600);
}

.palette__form { margin: 0; }

.palette__box {
  width: 100%;
  padding: 0.9rem 1.1rem;
  border: 0;
  border-bottom: 1px solid var(--color-slate-200);
  font-size: 1rem;
  color: var(--color-slate-900);
  background: var(--color-white);
}

/* The ring is drawn inside rather than around. The box is full bleed against
   the top of the dialog, so an outline at the usual 2px offset is clipped by
   the dialog's own rounded corner and reads as a stray line. Same ring, same
   width, same colour — only the side of the edge it sits on differs. The ring
   used to be suppressed here with nothing put in its place, which left the
   app's own keyboard feature as the one input that never said it had the
   keyboard. */
.palette__box:focus-visible { outline-offset: -2px; }
.palette__box::placeholder { color: var(--color-slate-400); }

/* The list, capped so a long answer scrolls inside the box rather than pushing
   it off the screen — the highlight is kept in view by the controller. */
.palette__results {
  display: block;
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.35rem;
}

.palette__group { padding: 0.25rem 0; }
.palette__group + .palette__group { border-top: 1px solid var(--color-slate-100); }

.palette__label {
  padding: 0.4rem 0.65rem 0.2rem;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-slate-400);
}

.palette__option,
.palette__more {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.45rem 0.65rem;
  border-radius: 0.5rem;
  color: var(--color-slate-900);
  text-decoration: none;
  cursor: pointer;
}

/* Highlight is one class the controller moves, so hover and keyboard land on
   the same look rather than two that drift apart. */
.palette__option:hover,
.palette__more:hover,
.palette__option--on { background: var(--color-slate-100); }

.palette__name { font-size: 0.9rem; font-weight: 500; }

.palette__where {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--color-slate-400);
  white-space: nowrap;
}

.palette__more { font-size: 0.85rem; color: var(--color-slate-600); }

.palette__hint {
  padding: 1.1rem;
  font-size: 0.875rem;
  color: var(--color-slate-600);
}

/* The foot of the jump box: what the keys do, and the way back to the page
   this opened over.
 *
 * It is one row rather than a line of help text, because the right half is a
 * real button. Escape has always closed this box and nothing said so, which
 * left somebody who opened it by accident with no way out they could see.
 *
 * The keys are drawn the same way the menu bar's ⌘K is — same border, same
 * grey, same size — so a key on the page always looks like a key. */
.palette__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.6rem 0.5rem 1rem;
  border-top: 1px solid var(--color-slate-200);
  background: var(--color-slate-50);
  font-size: 0.75rem;
  color: var(--color-slate-500);
}

.palette__keys { display: flex; align-items: center; gap: 0.3rem; }

.palette__back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.55rem;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  font: inherit;
  color: var(--color-slate-600);
  cursor: pointer;
}

.palette__back:hover { color: var(--color-slate-900); background: var(--color-slate-100); }

.palette__foot kbd {
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.3rem;
  background: var(--color-white);
  font-size: 0.7rem;
  font-family: inherit;
  color: var(--color-slate-400);
}

/* A phone has no arrow keys and no Escape, so the half of this that names
   them says nothing there. The way back is the half that matters on a touch
   screen, and it stays. */
@media (max-width: 40rem) {
  .palette__keys { display: none; }
  .palette__foot { justify-content: flex-end; }
}

/* The trigger in the menubar. Pushed to the far end, because it opens what is
   already on the page rather than being one more place to go. */
.menubar__find {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  background: var(--color-white);
  font-size: 0.8rem;
  color: var(--color-slate-600);
  cursor: pointer;
  transition: color 120ms ease-out, border-color 120ms ease-out;
}

.menubar__find:hover { color: var(--color-slate-900); border-color: var(--color-slate-600); }

.menubar__key {
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.3rem;
  background: var(--color-slate-100);
  font-size: 0.7rem;
  font-family: inherit;
  color: var(--color-slate-400);
}

/* Narrow screens have no room for a keyboard nobody there has — the label
   carries it, and the hotkey stays for those who do. */
@media (max-width: 30rem) {
  .menubar__key { display: none; }
}

/*
 * And below the width where the whole bar still fits, two labels go.
 *
 * The menu bar is a row that cannot wrap — the brand, three menus, and this
 * button — and none of those widths depend on the screen. They add up to
 * 440px. A phone is 390px. So the button, being last, was laid out from
 * 387px to 440px: three pixels of it on the screen, its label squeezed to
 * nothing, and `overflow-x: hidden` on <body> (see the top of this file)
 * taking the rest away rather than offering it as a sideways scroll. That is
 * why it read as cut off at the top right rather than as merely far over.
 *
 * What goes is words, never a destination: the button keeps its drawing, and
 * the brand keeps the mark that is already the app's icon. Both labels are
 * hidden the way `sr-only` hides one — still in the document, still the
 * accessible name — so what a screen reader is told does not change, and
 * neither does anything wider than 40rem.
 */
@media (max-width: 40rem) {
  .menubar__find { padding: 0.35rem 0.45rem; }
  .menubar__find-label { position: absolute; width: 1px; height: 1px; padding: 0;
    margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}

/* The wordmark holds on for another 10rem, because between the two widths the
   bar still has room for it once the button above has given its words up. */
@media (max-width: 30rem) {
  .menubar {
    /* Tighter on the narrowest screens, and still clear of the notch. */
    padding:
      calc(0.6rem + env(safe-area-inset-top, 0px))
      calc(0.7rem + env(safe-area-inset-right, 0px))
      0.6rem
      calc(0.7rem + env(safe-area-inset-left, 0px));
    gap: 0.35rem;
  }
  .menubar__brand-name { position: absolute; width: 1px; height: 1px; padding: 0;
    margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}

.outbox-review__list {
  margin: 0 0 1.25rem;
  padding: 0;
  list-style: none;
  max-height: 60vh;
  overflow-y: auto;
}

.outbox-review__item {
  padding: 0.75rem 0;
  border-top: 1px solid var(--color-slate-200);
}

.outbox-review__item:first-child { border-top: 0; padding-top: 0; }

.outbox-review__summary {
  margin: 0 0 0.5rem;
  font-size: 0.875rem;
  color: var(--color-slate-900);
}

.outbox-review__actions { display: flex; gap: 0.5rem; }

.outbox-review__actions button {
  border-radius: 0.375rem;
  padding: 0.375rem 0.75rem;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
}

.outbox-review__restore { border: 0; background: var(--color-accent); color: var(--color-on-fill); }
.outbox-review__restore:hover { filter: brightness(0.92); }
.outbox-review__restore:disabled { background: var(--color-slate-400); cursor: not-allowed; }

.outbox-review__discard { border: 1px solid var(--color-slate-200); background: var(--color-white); color: var(--color-slate-900); }
.outbox-review__discard:hover { border-color: var(--color-slate-600); }

.outbox-review__compare {
  width: 100%;
  margin: 0 0 0.75rem;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  border-collapse: collapse;
  font-size: 0.8125rem;
  overflow: hidden;
}

.outbox-review__compare caption {
  caption-side: top;
  text-align: left;
  padding: 0 0 0.4rem;
  color: var(--color-slate-500);
}

.outbox-review__compare th,
.outbox-review__compare td {
  padding: 0.4rem 0.6rem;
  text-align: left;
  vertical-align: top;
  border-top: 1px solid var(--color-slate-200);
}

.outbox-review__compare thead th {
  border-top: 0;
  background: var(--color-slate-50);
  color: var(--color-slate-500);
  font-weight: 500;
}

.outbox-review__compare tbody th { font-weight: 500; color: var(--color-slate-900); }

.outbox-review__changed td { font-weight: 600; }

/*
 * Rich text as it reads back. The editor ships its own styles for the editing
 * surface; this is the same content once it is just content again.
 */
.rich-text > :first-child { margin-top: 0; }
.rich-text > :last-child { margin-bottom: 0; }
.rich-text p { margin: 0 0 0.75em; }
.rich-text h1, .rich-text h2, .rich-text h3,
.rich-text h4, .rich-text h5, .rich-text h6 {
  margin: 1.25em 0 0.5em;
  font-weight: 600;
  line-height: 1.25;
}
.rich-text h1 { font-size: 1.25rem; }
.rich-text h2 { font-size: 1.125rem; }
.rich-text h3 { font-size: 1rem; }
.rich-text ul, .rich-text ol { margin: 0 0 0.75em; padding-left: 1.25rem; }
.rich-text ul { list-style: disc; }
.rich-text ol { list-style: decimal; }
.rich-text li { margin: 0.125em 0; }
.rich-text a { color: var(--color-blue-700); text-decoration: underline; }
.rich-text blockquote {
  margin: 0 0 0.75em;
  padding-left: 0.75rem;
  border-left: 2px solid var(--color-slate-200);
  color: var(--color-slate-600);
}
.rich-text pre {
  margin: 0 0 0.75em;
  padding: 0.625rem 0.75rem;
  overflow-x: auto;
  background: var(--color-slate-100);
  border: 1px solid var(--color-slate-200);
  border-radius: 0.375rem;
  font-size: 0.8125rem;
}
.rich-text code { font-size: 0.875em; }
.rich-text hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-slate-200); }

/* ==========================================================================
 * The warm kit.
 *
 * Not a desktop — a box of labelled parts. One accent, spent on the single
 * action that matters on a given screen; everything else stays quiet, warm
 * neutral, and softly rounded, and depth comes from a faint lift rather than
 * a bevel.
 *
 * It lives beside the Tailwind theme rather than inside it: the theme moved
 * what the colours mean, and this adds the furniture utilities cannot
 * express — the menu bar, the status bar, the tree, the pager. Selectors are
 * kept to elements and to the app's own region classes wherever possible;
 * where a utility has to be overridden it is qualified by an ancestor rather
 * than shouted at with !important.
 * ========================================================================== */

::selection { color: var(--color-accent-ink); background: var(--color-accent); }

/*
 * Tailwind wraps its own rules in `@layer base, components, utilities`, and
 * a layer always wins over an unlayered declaration of the same or lower
 * specificity that comes before it in the layer order — but an *unlayered*
 * rule outranks every layer regardless of specificity. Every plain-element
 * default below is deliberately placed in `base` so that a template's own
 * `text-slate-500`, `rounded-full` or similar utility — living in the
 * higher-priority `utilities` layer — keeps overriding it as expected.
 */
@layer base {
  /* ---- Links --------------------------------------------------------------- */

  a { color: var(--color-accent); }
  a:hover { color: var(--color-accent-strong); }

  /* Focus is a solid accent ring rather than the browser default, and it is
     the same ring everywhere a control can be reached by keyboard. */
  :focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  /* Rounds what Tailwind's own radius tokens don't reach: native form
     controls with no explicit `rounded-*` utility of their own. */
  button, [type="submit"], [type="button"] {
    border-radius: var(--radius-md);
    cursor: pointer;
  }
}

/* ---- Controls -------------------------------------------------------------
 *
 * The primary action used to be described here, in a block of `!important`
 * that repainted every ink-coloured button in the accent — because templates
 * wrote `bg-slate-900` and the theme's accent token went unused. It is now
 * described once in `ButtonsHelper`, which reaches for `bg-accent` directly,
 * so there is nothing left to repaint and the fill, the lift under the
 * pointer and the press are ordinary utilities on the button itself.
 *
 * One rule survives that move. A label on a filled button has to read against
 * the fill, and `a { color }` in the base layer above is close enough to the
 * `text-accent-ink` utility that the outcome should not rest on which layer
 * wins: an accent label on an accent button is invisible, and that is too
 * quiet a way to fail. Stated plainly here, it cannot happen.
 */

main a.bg-accent,
.page-actions a.bg-accent {
  color: var(--color-accent-ink);
}

/* ---- A control is painted, not left transparent.
 *
 * Preflight gives every form control `background-color: transparent`, and on
 * the page that reads correctly in both schemes: the ground shows through and
 * the ground turns over with the theme. It is wrong in the one place this file
 * cannot see. **A select's list is drawn by the browser**, and a browser
 * handed a transparent control has nothing to draw that list from, so it
 * reaches for its own default — a white sheet, over a dark page, with this
 * app's near-white text on it.
 *
 * `color-scheme` above is what hands the *browser's* furniture to the reader's
 * preference, and it is not enough on its own: it decides which default, not
 * which colour. So a control says its own, in the tokens that turn over, and
 * the list follows the field it belongs to.
 *
 * `--color-white` is the card face rather than the colour white — see the dark
 * block in tailwind/application.css. A field is a thing you can act on sitting
 * on the page, which is the same claim a card makes.
 *
 * **A button is not one of these.** `input` covers `type="submit"` too, and an
 * element-plus-attribute selector outweighs a class, so this rule quietly beat
 * the `bg-accent` on every button drawn with `form.submit` — which is the main
 * action of nearly every form in the app, the Log in button on the first page
 * anybody sees included. They were painted the card face instead: the one
 * colour this whole design spends on the one thing worth doing on a screen,
 * missing from the button that does it, on a page that otherwise looked right.
 *
 * A button has no list to open and nothing for the browser to draw from it, so
 * it was never what this rule was for. `ButtonsHelper` says what a button
 * looks like, and it is the only thing that should.
 */
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]),
select,
textarea,
trix-editor,
lexxy-editor {
  border-radius: var(--radius-md);
  background-color: var(--color-white);
  color: var(--color-slate-900);
}

/* The rows of that list, for the browsers that paint them from here rather
 * than from the select. Said separately because an <option> is not a control
 * and inherits nothing useful from one. */
option,
optgroup {
  background-color: var(--color-white);
  color: var(--color-slate-900);
}

/* A placeholder is not text somebody wrote, and on a painted field it must not
 * read as if it were. */
::placeholder {
  color: var(--color-slate-400);
  opacity: 1;
}

/* No focus rule for form controls here. There was one — the same accent ring
 * at a 1px offset, unlayered, and unlayered outranks every layer regardless of
 * specificity, so it beat the `:focus-visible` ring in the base layer for every
 * input, select and textarea in the app. Two rings, two offsets, and the wrong
 * one won. It was also `:focus` rather than `:focus-visible`, so a select drew
 * a ring when clicked with a mouse, which nothing else here does.
 *
 * The base layer covers all of it. A text input still rings when clicked,
 * because `:focus-visible` matches an element that takes keyboard input however
 * focus arrived; a select no longer does, because it does not.
 */

[type="checkbox"], [type="radio"] { accent-color: var(--color-accent); }

/* ---- The bubble a hint says its piece in.
 *
 * Drawn here and nowhere else. It used to be eleven utility classes in
 * `HintsHelper` and three `!important` overrides down here fighting them, and
 * what that arrangement lost was the text colour: the override repainted the
 * background warm and left `text-white` on the span. `--color-white` is this
 * theme's card face rather than the colour white and turns over with the
 * scheme, so the words were white on cream in the light scheme and near-black
 * on near-black in the dark one — unreadable in both. Two places describing
 * one bubble is exactly the drift the note here claimed to prevent.
 *
 * The colours are a pair on purpose: 50 is the face and 900 is the ink, and
 * both flip with the scheme, so the contrast holds without this knowing which
 * scheme it is in.
 *
 * `normal-case` and `tracking-normal` are not decoration — a hint sits inside
 * section headings that are `uppercase tracking-wide`, and a sentence of
 * explanation set in spaced capitals is a sentence nobody reads. */
.hint-bubble {
  position: absolute;
  bottom: 100%;
  left: 50%;
  translate: -50% 0;
  z-index: 10;
  margin-bottom: 0.5rem;
  width: 16rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-amber-800);
  border-radius: 0.5rem;
  background: var(--color-amber-50);
  color: var(--color-amber-900);
  font-size: 0.75rem;
  line-height: 1rem;
  font-weight: 400;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  pointer-events: none;
}

/* On a phone it is a sheet at the foot of the screen rather than a card over
 * the thing it explains.
 *
 * 16rem hung off the middle of a "?" needs 8rem of room on each side of it,
 * and a "?" sits at the end of a heading — so on a 390px screen the bubble ran
 * off whichever edge its heading ended near, and `overflow-x: hidden` on the
 * body cut the words off mid-sentence. CSS cannot know where in the viewport
 * an absolutely positioned element has landed, so the only fix that always
 * works is to stop positioning it against the "?" at all.
 *
 * Above the status bar, which is sticky and would otherwise sit on top of it.
 * Still `pointer-events: none`, so the sheet never takes the tap meant for
 * whatever is under it, and still opened by hover or focus alone — a tap
 * focuses the "?", which is the touch path this has always relied on. */
@media (max-width: 40rem) {
  .hint-bubble {
    position: fixed;
    left: 1rem;
    right: 1rem;
    bottom: 3.5rem;
    top: auto;
    width: auto;
    margin-bottom: 0;
    translate: none;
    z-index: 45;
    box-shadow: var(--shadow-lg);
  }
}

.preview-panel {
  border-radius: 0.625rem !important;
  box-shadow: var(--shadow-lg);
}

.preview-panel--right {
  top: 0;
  left: 100%;
  margin-left: 0.5rem;
}

.preview-panel--above {
  bottom: 100%;
  left: 0;
  margin-bottom: 0.5rem;
}

/* On a phone a preview is a sheet at the foot of the screen rather than a
 * card beside the thing it explains — the same fix hints use. Anchor
 * positioning cannot know where in the viewport a panel has landed either;
 * on a narrow screen the only fix that always works is to stop tethering it
 * to the trigger at all. */
@media (max-width: 40rem) {
  .preview-panel {
    position: fixed;
    left: 1rem;
    right: 1rem;
    bottom: 3.5rem;
    top: auto;
    width: auto;
    margin: 0;
    translate: none;
    z-index: 45;
  }
}

/*
 * `anchor-scope` is what keeps eight previews on one page from sharing one
 * name. Without it, every panel would tether to the last `.preview` in the
 * document. Browsers that can name an anchor but cannot scope it keep the
 * absolute offsets above, which is the old behaviour and still correct.
 */
@supports (anchor-name: --x) and (anchor-scope: --x) {
  .preview {
    anchor-name: --preview;
    anchor-scope: --preview;
  }

  .preview-panel {
    position: fixed;
    position-anchor: --preview;
    margin: 0;
  }

  .preview-panel--right {
    top: anchor(top);
    left: calc(anchor(right) + 0.5rem);
    bottom: auto;
    right: auto;
    position-try-fallbacks: flip-inline, flip-block;
  }

  .preview-panel--above {
    top: auto;
    bottom: calc(anchor(top) + 0.5rem);
    left: anchor(left);
    right: auto;
    position-try-fallbacks: flip-block, flip-inline;
  }

  @media (max-width: 40rem) {
    .preview-panel {
      position: fixed;
      left: 1rem;
      right: 1rem;
      bottom: 3.5rem;
      top: auto;
      width: auto;
      position-try-fallbacks: none;
    }
  }
}

/* ---- Form pages -------------------------------------------------------
 *
 * Every new/edit page sits in the same centred card. Named once so a
 * template cannot drift from the others, and so the lift lives with the
 * rest of the drawing rather than as a qualifier on a copied utility
 * string. A border alone reads flatter here than the menu and toast
 * surfaces sitting on the same paper.
 */
.form-sheet {
  max-width: 36rem;
  margin: 2rem auto;
  padding: 2.25rem 2rem;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-xl);
  background: var(--color-white);
  box-shadow: var(--shadow-md);
}

/* Compact fields sit beside a neighbour when they fit; notes, files and
   titles keep a whole line. Same wrapping trick as .composer-fields--wide,
   so a long row form does not become a strip of one-field pages. */
.form-fields {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 1.25rem 0.75rem;
}

.form-fields__wide { grid-column: 1 / -1; }

/* The same face without the centring. A list, a folded block, a table
 * wrapper and a tile all sat on this surface, each writing the utilities
 * out. Padding and hover stay on the template: those are not shared.
 */
.sheet {
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  box-shadow: var(--shadow-sm);
}

/* A table in a sheet has a head you can see as a band: a faint fill across
 * the column names, so the block reads as a head and a body rather than one
 * list with a different first line. The corners follow the sheet's, because
 * the sheet does not clip (a preview hangs out of it). A pinned head cell
 * wears the same fill, or the freeze would show as a white gap. */
.sheet table > thead > tr { background: var(--color-slate-50); }
.sheet table > thead > tr > th:first-child { border-top-left-radius: calc(var(--radius-lg) - 1px); }
.sheet table > thead > tr > th:last-child { border-top-right-radius: calc(var(--radius-lg) - 1px); }
.sheet thead [data-stuck] { background-color: var(--color-slate-50); }

/* What a form says when it cannot save. Named once so a new form cannot
 * invent a second red box.
 */
.fault {
  border-radius: var(--radius-md);
  padding: 1rem;
  background: var(--color-red-50);
  color: var(--color-red-800);
  font-size: 0.875rem;
}

/* The field a save refused, marked where it is as well as in the strip
 * above: its message under the label, in the strip's own ink, and its box
 * edged in the same red. The message sits inside the <label>, so it is part
 * of the input's name for a screen reader. See items/_form. */
.field-error {
  display: block;
  margin-top: 0.125rem;
  font-weight: 400;
  color: var(--color-red-800);
}
.field--invalid :is(input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select, textarea) {
  border-color: var(--color-red-500);
}

/* A sample of text the machine would run. Ink on purpose: this is a
 * listing, not a primary action.
 */
.code {
  overflow-x: auto;
  border-radius: var(--radius-md);
  padding: 0.5rem 0.75rem;
  background: var(--color-slate-900);
  color: var(--color-slate-100);
  font-size: 0.75rem;
  /* A listing is characters a machine reads back — a key, a token, a payload —
   * so it is drawn in a fixed-width face where every character is one column
   * and nothing is a letter it is not. The stack is the reader's own system
   * monospace, in the same spirit as the rounded system face the rest of the
   * app falls back to: nothing to fetch, nothing to precache, nothing that can
   * fail with no connection. Never a Google font — see docs/design.md. */
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono",
    "Roboto Mono", Menlo, Consolas, monospace;
}

/* ---- Skip to content -------------------------------------------------------
 *
 * The first thing in the document and the first thing Tab reaches, drawn only
 * while it holds focus. It is moved off the top of the screen rather than
 * hidden: `display: none` and `visibility: hidden` both take an element out of
 * the tab order, which would leave the link unreachable by the one person it
 * is for. `transform` rather than `top`, so nothing about the page reflows
 * when it arrives.
 */

.skip-link {
  position: fixed;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 60;
  padding: 0.45rem 0.9rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-accent-ink);
  background: var(--color-accent);
  border-radius: var(--radius-md);
  text-decoration: none;
  transform: translateY(-250%);
}

/* `:focus-visible`, not `:focus`, for the same reason as every other rule in
   this file: `:focus` draws on a pointer as well. Nothing changes here in
   practice — the link sits off the top of the screen, so the keyboard is the
   only thing that can reach it — but one spelling for focus is the rule. */
.skip-link:focus-visible { transform: translateY(0); }

/* ---- A write in flight -----------------------------------------------------
 *
 * Nothing used to say a write was happening. An inline edit saving, a batch of
 * forty deleting and a card dragged to another day looked exactly like nothing
 * happening until the answer arrived. See app/javascript/writing.js, which
 * puts `data-writing` on <html> for as long as a write is in the air, and
 * docs/design.md.
 *
 * **The delay lives here rather than in a timer.** A write answered in forty
 * milliseconds should show nothing at all — a bar that flashes on every
 * keystroke-and-Enter is noise, and worse than silence. So the state is set
 * the moment the write starts and the *drawing* of it waits: the rules below
 * carry a transition delay on the way in and none on the way out, which means
 * a quick write finishes before its own bar has begun to appear, and a slow
 * one fades in and then leaves the instant it lands. No JavaScript is involved
 * in the timing, so nothing has to be cancelled, and two overlapping writes
 * cannot leave a timer behind.
 *
 * `aria-busy` is Turbo's own, set on the document while a visit is being
 * fetched. The bar answers to both, because a page being fetched and a write
 * being sent are the same news to somebody waiting.
 */

.writing {
  position: fixed;
  top: 0;
  inset-inline: 0;
  z-index: 60;
  height: 3px;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms linear, visibility 0s linear 120ms;
}

html[data-writing] .writing,
html[aria-busy="true"] .writing {
  opacity: 1;
  visibility: visible;
  transition: opacity 140ms linear 400ms, visibility 0s linear 400ms;
}

/* The pointer's half of the same news, and the half with no delay: a cursor
   is already under somebody's attention, so it can say "waiting" at once
   without taking any of the page's room to do it. */
html[data-writing] { cursor: progress; }

.writing__bar {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--color-accent);
}

/* Indeterminate on purpose. The app does not know how far through a write is
   — there is one request and it either has come back or has not — so a bar
   that filled up would be inventing a number. This one only says "still".
   Under a reduced-motion preference it stays the full-width rule above:
   a still bar says the same thing, quietly. */
@media (prefers-reduced-motion: no-preference) {
  .writing__bar {
    width: 35%;
    animation: writing-slide 1.15s ease-in-out infinite;
  }

  @keyframes writing-slide {
    from { transform: translateX(-100%); }
    to { transform: translateX(300%); }
  }
}

/* A write answered into a frame — an inline edit, a drag, anything from the
   column menu — already says which part of the page is waiting, because Turbo
   marks the frame itself. Faded rather than covered or emptied: the rows stay
   readable and stay where they are, so nothing moves under a pointer and
   nobody loses their place. The same delay as the bar, for the same reason. */
turbo-frame { transition: opacity 120ms linear; }

turbo-frame[busy] {
  opacity: 0.6;
  transition: opacity 140ms linear 400ms;
}

/* The region the link lands on, and the one place in the app that takes focus
   without drawing the ring. `main` is a landmark, not a control: it is out of
   the tab order, it does nothing when it holds focus, and an accent outline
   around the whole page reads as an error rather than as an answer. The
   confirmation of the jump is the next Tab press landing inside the content.
   `test/invariants_test.rb` names this rule as its only exception. */
main:focus-visible { outline: none; }

/* ---- Menu bar --------------------------------------------------------------
 *
 * A real destination behind every item, same as before — only the drawing
 * changed: a clean bar instead of a raised strip, an accent mark for the
 * brand instead of a system font quirk.
 */

.menubar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* The top of the screen. With viewport-fit=cover the bar meets the notch,
     so the top and the sides pad back by the safe-area insets. env() is 0
     where there are none, so this is the same 0.6rem 1rem everywhere else.
     The canvas tone fills the inset too, so the bar reads as one strip to the
     edge rather than a band under a bare notch. */
  padding:
    calc(0.6rem + env(safe-area-inset-top, 0px))
    calc(1rem + env(safe-area-inset-right, 0px))
    0.6rem
    calc(1rem + env(safe-area-inset-left, 0px));
  /* The ground itself, the same as the sidebar. The chrome — the strip at the
     top and the tree down the side — is not a surface at all, so the white
     blocks of the page are the only faces on the screen. No rule under it:
     the edge of the first block is the line. See "Blocks". */
  background: var(--color-slate-100);
}

.menubar__brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0.4rem;
  font-weight: 700;
  color: var(--color-slate-900);
  text-decoration: none;
}

.menubar__brand::before {
  content: "";
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 0.3rem;
  background: linear-gradient(135deg, var(--color-accent), var(--color-brand-warm));
}

.menubar__menus { display: flex; gap: 0.15rem; }

.menu { position: relative; }

.menu > summary {
  padding: 0.35rem 0.7rem;
  list-style: none;
  cursor: pointer;
  border-radius: 0.5rem;
  color: var(--color-slate-600);
  transition: color 120ms ease-out, background-color 120ms ease-out;
}

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

.menu > summary:hover,
.menu[open] > summary { color: var(--color-slate-900); background: var(--color-slate-100); }

/* On the menu bar the ground is slate-100 already, so a menu that is open is
   a small block lifted off it rather than a fill the same colour as the bar. */
.menubar .menu > summary:hover,
.menubar .menu[open] > summary { background: var(--color-white); box-shadow: var(--shadow-sm); }

.menu__items {
  position: absolute;
  z-index: 40;
  top: calc(100% + 0.35rem);
  left: 0;
  min-width: 12rem;
  padding: 0.35rem;
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: 0.75rem;
  box-shadow: var(--shadow-lg);
}

.menu__items a,
.menu__items button {
  display: block;
  width: 100%;
  min-width: 0;
  padding: 0.4rem 0.65rem;
  font: inherit;
  text-align: left;
  color: var(--color-slate-900);
  text-decoration: none;
  background: transparent;
  border: 0;
  border-radius: 0.5rem;
  cursor: pointer;
}

.menu__items a:hover,
.menu__items button:hover { color: var(--color-slate-900); background: var(--color-slate-100); }

.menu__items form { margin: 0; }

/* Hung off the trigger's right edge rather than its left, for a menu that
 * sits at the right of its box — a card in the workspace grid, where a
 * left-aligned panel on the rightmost card runs off the page. */
.menu__items--right { left: auto; right: 0; }

.menu__items button[aria-pressed="true"] {
  background: var(--color-slate-100);
  font-weight: 600;
}

.menu__items hr { margin: 0.3rem 0.2rem; border: 0; border-top: 1px solid var(--color-slate-200); }

/* ---- Activity ---------------------------------------------------------
 *
 * The one menu that holds a list rather than a set of destinations, so it is
 * the one that needs a width of its own and a scroll. Everything above styles
 * a menu item as a full-width block; an entry here is three lines, and only
 * the middle one is a link.
 */
.menu__items--wide {
  /* Activity sits with File and View, on the left of the bar. Hanging this
     22rem panel off its right-hand edge (`right: 0`) ran the list off the
     left of the screen — on a phone, and on a desk whenever the three menus
     together were narrower than the panel. It opens to the right of the
     trigger, the same way the menus beside it do, where the bar still has
     room. */
  left: 0;
  right: auto;
  width: 22rem;
  max-width: calc(100vw - 1.5rem);
  max-height: 70vh;
  overflow-y: auto;
}

/* On a phone it is a sheet at the foot of the screen rather than a card
 * hung off the word "Activity".
 *
 * 22rem from a trigger that sits after File and View still needs more room
 * than a 390px screen has on either side, so `left: 0` above would run it
 * off the right instead. CSS cannot know where in the viewport an
 * absolutely positioned element has landed, so the only fix that always
 * works is to stop positioning it against the trigger at all — the same
 * answer hints and previews already use.
 *
 * Above the status bar, which is sticky and would otherwise sit on top of
 * it. The heading inside the panel is what says what this is, because the
 * summary that opened it is now at the other end of the screen. */
@media (max-width: 40rem) {
  .menu__items--wide {
    position: fixed;
    /* A sheet at the foot of the phone. It clears the status bar (3.5rem) and
       the home bar below it, and stands off the side notch in landscape. */
    left: calc(1rem + env(safe-area-inset-left, 0px));
    right: calc(1rem + env(safe-area-inset-right, 0px));
    bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
    top: auto;
    width: auto;
    max-width: none;
    max-height: min(70vh, calc(100dvh - 8rem));
    z-index: 45;
  }
}

/* ---- A selection's own bar -------------------------------------------------
 *
 * It grew to six actions one pull request at a time, and every one of them
 * arrived wearing the same button as the last, in the same row, with the
 * irreversible one told apart by being red and by being at the end. That is
 * three kinds of thing — a form, a read and a write — drawn as one kind.
 *
 * The bar says which is which by weight rather than by arrangement, so the
 * order can stay the order somebody learned:
 *
 *   the count    a sentence, no border, nothing to press
 *   the form     the only part with inputs in it, and the only filled button
 *   a read       quiet, because Export and Move change nothing
 *   a write      bordered, because it happens when pressed
 *   Delete       red and past a rule, because the difference between it and
 *                Archive is not a shade — it is whether the rows exist after
 *
 * Every button here is `size: :sm`: they are told one height rather than
 * happening to be one, which is what keeps a strip of six from stepping.
 */
.batch-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  margin-top: 0.75rem;
  padding: 0.6rem 0.9rem;
  font-size: 0.875rem;
  background: var(--color-slate-50);
  border: 1px solid var(--color-slate-300);
  border-radius: var(--radius-lg);
}

.batch-bar__count {
  margin: 0;
  font-weight: 500;
  color: var(--color-slate-700);
}

.batch-bar__set {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  color: var(--color-slate-500);
}

/* The value widget is shared with the automation form, which is a column of
   stacked fields, so it is drawn `w-full`. In a strip that is the whole line,
   and it pushed the button that submits it onto a third one. A width here
   rather than a change there: the partial is right about the form it was
   written for, and a bar is entitled to say how wide things are in a bar. */
.batch-bar__set input,
.batch-bar__set select {
  width: 11rem;
  min-width: 0;
}

/* `margin-inline-start: auto` rather than a spacer element: the verbs sit at
   the far end on a wide screen, and on a narrow one they wrap to a line of
   their own, which is the same answer without a <span> that means nothing. */
.batch-bar__verbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  margin-inline-start: auto;
}

.batch-bar__apart {
  margin-inline-start: 0.3rem;
  padding-inline-start: 0.55rem;
  border-inline-start: 1px solid var(--color-slate-300);
}

/* Where there is no room for six verbs on one line, distance says what the
   rule said. A line that begins with a rule reads as something cut off. */
@media (max-width: 40rem) {
  .batch-bar__apart {
    margin-inline-start: auto;
    padding-inline-start: 0;
    border-inline-start: 0;
  }
}

/* The same in a narrow list on a wide screen; see "A table in a narrow list
   on a wide screen" below. */
@container rows (max-width: 40rem) {
  .batch-bar__apart {
    margin-inline-start: auto;
    padding-inline-start: 0;
    border-inline-start: 0;
  }
}

/* ---- A column's own menu ---------------------------------------------------
 *
 * The same list of items the menu bar draws, hung off a table heading instead.
 * It reuses `.menu__items` for the panel and adds only what a popover needs
 * that a <details> did not: the browser's own popover styles undone, and a
 * position of `auto` on every side so anchor positioning — or, where the
 * browser cannot anchor, `column_menu_controller` — is the only thing that
 * sets top and left.
 *
 * Why a popover at all is in items/_column_menu.html.erb: the table scrolls
 * sideways, and a box that scrolls on one axis clips the other.
 */
/*
 * A file cell or a card while a file is held over it: the accent ring the
 * keyboard's ring uses, so "this is where it lands" reads the same both ways.
 * See cell_edit_controller and file_drop_controller.
 */
td[data-file-over],
.card[data-file-over] { box-shadow: inset 0 0 0 2px var(--color-accent); }

/*
 * What letting go of a file over a type's page will do: one line, pinned to
 * the bottom of the window above everything, while the file is held. See
 * import_drop_controller.
 */
.import-drop {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  z-index: 50;
  transform: translateX(-50%);
  padding: 0.75rem 1.25rem;
  font-size: 0.875rem;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--color-accent);
}
.import-drop[hidden] { display: none; }

.column-menu { position: relative; display: inline-block; }

/*
 * The edge of a heading a reader drags to widen the column. A thin strip on
 * the heading's right-hand side, drawn only on hover or while dragging, so
 * the header row stays the quiet line it was. A heading pinned by
 * sticky_columns is already positioned; every other one is made so here.
 * See column_width_controller.
 */
th[data-column-width-url]:not([data-stuck]) { position: relative; }

.column-resize {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 0.5rem;
  cursor: col-resize;
  touch-action: none;
  border-right: 2px solid transparent;
}

.column-resize:hover,
.is-resizing-column .column-resize:active { border-right-color: var(--color-slate-300); }

.is-resizing-column,
.is-resizing-column * { cursor: col-resize !important; user-select: none; }

.column-menu__button {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  cursor: pointer;
}

.column-menu__button:hover { color: var(--color-slate-900); }

/* The kind's own mark, in the kind's own colour. It replaces the word that
   used to be printed under every heading in every table — see
   items/_table.html.erb. `align-self: center` because the button lines its
   children up on the baseline, and a drawing has none. */
.column-menu__mark {
  display: inline-flex;
  align-self: center;
}

/* And the word itself, at the top of the menu the heading opens: a heading
   rather than an item, so nothing here looks like something to press. */
.column-menu__kind {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  padding: 0.35rem 0.55rem;
  font-size: 0.75rem;
  color: var(--color-slate-500);
}

/* The caret is the only thing saying this heading opens, so it is always
   drawn: a control that appears when you point at it is a control nobody
   found. Faint enough that eight of them are not a row of arrows. */
.column-menu__caret {
  font-size: 0.6em;
  color: var(--color-slate-400);
}

/* Which column the table is in the order of. In the accent, because it is the
   one thing on a heading row worth finding at a glance. */
.column-menu__sorted { color: var(--color-accent); }

/* A popover is in the top layer but still a child of the heading in the
   document, so it inherits from it — and a table heading is drawn in small
   capitals with the letters spaced out. Undone here rather than on each item:
   `font: inherit` on a menu item is the wrong tool, because neither of these
   is part of the font shorthand, and one of the four items is a link, which
   inherits both where a button does not. */
.column-menu__items {
  position: fixed;
  inset: auto;
  margin: 0;
  max-height: 80vh;
  overflow-y: auto;
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.875rem;
  color: var(--color-slate-900);
}

.column-menu__items form { margin: 0; }

/*
 * A popover's implicit anchor is the button that opened it, so the menu
 * does not need a shared name — and a shared name would have pointed every
 * menu at the last heading. Hung from the button rather than the cell:
 * the kind mark now lives in the button, so the two are the same edge.
 */
@supports (anchor-name: --x) {
  .column-menu__items {
    position-anchor: auto;
    top: calc(anchor(bottom) + 4px);
    left: anchor(left);
    right: auto;
    position-try-fallbacks: flip-inline;
  }
}

.menubar__dot {
  display: inline-block;
  width: 0.45rem;
  height: 0.45rem;
  margin-left: 0.35rem;
  vertical-align: 0.1rem;
  background: var(--color-amber-500);
  border-radius: 9999px;
}

/* ---- Waiting ---------------------------------------------------------------
 *
 * A grey bar standing in for a line of text that has not arrived. Worth more
 * than the word "Loading" because it has the shape of the thing coming, so the
 * panel does not change size when the real content lands.
 *
 * The pulse is behind a motion query, like the rest of the movement in this
 * file. Without it the bar is simply a flat panel face, which still says
 * "something belongs here" — the meaning is in the shape, not the animation.
 *
 * `aria-hidden` on the markup, not here: there is nothing to read out, and the
 * frame it stands in already says what it is waiting for. */

.skeleton {
  border-radius: var(--radius-md);
  background: var(--color-slate-100);
}

@media (prefers-reduced-motion: no-preference) {
  .skeleton { animation: skeleton-pulse 1.6s ease-in-out infinite; }

  @keyframes skeleton-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
  }
}

.activity__list,
.activity__group-list { margin: 0; padding: 0; list-style: none; }

.activity__heading {
  display: none;
  margin: 0;
  padding: 0.55rem 0.65rem 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-slate-500);
}

@media (max-width: 40rem) {
  .activity__heading { display: block; }
}

.activity__group + .activity__group { border-top: 1px solid var(--color-slate-100); }

.activity__day {
  margin: 0;
  padding: 0.55rem 0.65rem 0.2rem;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--color-slate-500);
}

.activity__entry { border-radius: 0.5rem; }

.activity__entry + .activity__entry { border-top: 1px solid var(--color-slate-100); }

/* The whole row is the tap target. Overrides the block-level menu item
 * above, which is how File and View draw a destination, so an entry stays
 * three lines with a coloured dot rather than a padded label. */
.menu__items--wide .activity__hit {
  display: flex;
  gap: 0.6rem;
  width: 100%;
  min-width: 0;
  padding: 0.5rem 0.65rem;
  color: inherit;
  text-decoration: none;
  background: transparent;
  border-radius: 0.5rem;
}

.menu__items--wide a.activity__hit:hover {
  color: inherit;
  background: var(--color-slate-100);
  text-decoration: none;
}

.menu__items--wide a.activity__hit:hover .activity__detail { text-decoration: underline; }

@media (max-width: 40rem) {
  .menu__items--wide .activity__hit { padding: 0.7rem 0.75rem; }
}

/* What kind of thing happened, said in a colour before it is said in words —
 * the same six-colour vocabulary FieldKind already draws kinds in, so a
 * reader who has met one has met the other. */
.activity__kind {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  margin-top: 0.45rem;
  background: var(--color-slate-300);
  border-radius: 9999px;
}

.activity__kind--change { background: var(--color-blue-400); }
.activity__kind--import { background: var(--color-green-500); }
.activity__kind--automation { background: var(--color-purple-400); }
.activity__kind--webhook { background: var(--color-slate-400); }
.activity__kind--reminder { background: var(--color-amber-500); }
.activity__kind--problem { background: var(--color-red-500); }

.activity__body { min-width: 0; }

.activity__words {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-slate-700);
}

.activity__when { margin-left: 0.35rem; font-weight: 400; color: var(--color-slate-400); }

.activity__detail {
  margin: 0.1rem 0 0;
  font-size: 0.875rem;
  color: var(--color-slate-900);
  overflow-wrap: anywhere;
}

.activity__where,
.activity__note,
.activity__empty {
  margin: 0.1rem 0 0;
  font-size: 0.75rem;
  color: var(--color-slate-400);
}

.activity__note { padding: 0.5rem 0.65rem 0.2rem; border-top: 1px solid var(--color-slate-100); }
.activity__empty { padding: 0.5rem 0.65rem; }

/* ---- Notifications ----------------------------------------------------
 *
 * A rounded card with a coloured edge saying what kind of thing this is,
 * rather than a title bar borrowed from a window.
 *
 * Above the menus (z-index 40) on purpose: a menu left open should not hide
 * the thing that just arrived. Below nothing else, because a <dialog> draws
 * on the top layer and beats any z-index there is — so a confirmation still
 * comes first, which is right: it is a question, and this is only an answer.
 */

.toasts {
  position: fixed;
  /* Below the menu bar, not over it. Every write draws one of these now, and
     at 0.75rem the stack sat exactly on top of the jump box and the menus —
     a popup that eats the button you were reaching for is worse than one you
     have to look for. */
  /* Below the menu bar, which itself grew by the notch inset, and clear of a
     rounded top-right corner. env() is 0 where there is neither. */
  top: calc(3.25rem + env(safe-area-inset-top, 0px));
  right: calc(0.75rem + env(safe-area-inset-right, 0px));
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: min(19rem, calc(100vw - 1rem));
  /* The stack itself is not a target. It stands over the top corner of every
     page whether it holds anything or not, and only what is in it should ever
     take a click. */
  pointer-events: none;
}

.toasts:empty { display: none; }

.toast { pointer-events: auto; }

.toast {
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-left: 3px solid var(--color-accent);
  border-radius: 0.75rem;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  animation: toast-in 180ms ease-out;
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes toast-in {
    from { opacity: 0; transform: translateY(-0.5rem); }
  }
}

/* What a write said, and what went wrong, told apart by the one edge the card
   already had. A notice is an answer; an alert is a problem, and reads like
   one without depending on the words to carry it alone. */
/* The two shades this stylesheet already uses elsewhere, rather than two new
   ones: Tailwind emits only the variables something asks for, so a shade
   nothing else wanted would resolve to nothing at all. */
.toast--notice { border-left-color: var(--color-emerald-700); }
.toast--alert { border-left-color: var(--color-red-700); }

/* ---- Arriving and leaving -----------------------------------------------
 *
 * Everything the app raises over a page used to arrive in one frame and go in
 * one frame: a menu, the column menu, the three dialogs, and a toast on its
 * way out. The toast and the row panel had an entrance; nothing had an exit,
 * so every close was a jump.
 *
 * Each one fades now, and the thing that holds a list of choices drops the
 * last quarter-rem into place. 120ms both ways — under the 150ms the page
 * cross-fade takes, because these are smaller than a page. No direction
 * beyond that: a menu opens from its trigger and closes back into it, which
 * is where the eye already is.
 *
 * All of it is CSS, and all of it sits behind the `no-preference` gate, so
 * somebody who asked for less movement gets exactly the snap they had. The
 * exits need the browser to keep a closing element drawn for as long as the
 * fade: `display` and `overlay` with `allow-discrete` do that for a dialog
 * and a popover, `content-visibility` on `::details-content` does it for a
 * <details>. A browser without those snaps shut, as before. The toast is the
 * one exit JavaScript waits for, because it is removed from the document
 * rather than closed — see app/javascript/leave.js.
 */
@media (prefers-reduced-motion: no-preference) {
  .confirm,
  .palette,
  .shortcuts,
  .confirm::backdrop,
  .palette::backdrop,
  .shortcuts::backdrop {
    opacity: 0;
    transition:
      opacity 120ms ease-out,
      overlay 120ms allow-discrete,
      display 120ms allow-discrete;
  }

  .confirm[open],
  .palette[open],
  .shortcuts[open],
  .confirm[open]::backdrop,
  .palette[open]::backdrop,
  .shortcuts[open]::backdrop { opacity: 1; }

  @starting-style {
    .confirm[open],
    .palette[open],
    .shortcuts[open],
    .confirm[open]::backdrop,
    .palette[open]::backdrop,
    .shortcuts[open]::backdrop { opacity: 0; }
  }

  /* A menu bar menu, a card's action menu, a view's. The <details> keeps its
     content drawn through the fade; the list itself does the moving. */
  .menu::details-content {
    transition: content-visibility 120ms allow-discrete;
  }

  .menu > .menu__items {
    transition: opacity 120ms ease-out, translate 120ms ease-out;
  }

  .menu:not([open]) > .menu__items {
    opacity: 0;
    translate: 0 -0.25rem;
    pointer-events: none;
  }

  @starting-style {
    .menu[open] > .menu__items { opacity: 0; translate: 0 -0.25rem; }
  }

  /* The column menu is a popover, so it closes the way a dialog does. */
  .column-menu__items {
    opacity: 0;
    translate: 0 -0.25rem;
    transition:
      opacity 120ms ease-out,
      translate 120ms ease-out,
      overlay 120ms allow-discrete,
      display 120ms allow-discrete;
  }

  .column-menu__items:popover-open { opacity: 1; translate: 0 0; }

  @starting-style {
    .column-menu__items:popover-open { opacity: 0; translate: 0 -0.25rem; }
  }

  /* Leaving the way it came in, and not a target on the way out. */
  .toast--leaving {
    opacity: 0;
    translate: 0 -0.5rem;
    pointer-events: none;
    transition: opacity 120ms ease-in, translate 120ms ease-in;
  }
}

/* The offer to put it back. A button, because it posts — see
   docs/responding.md — drawn as the quiet link it reads as. */
.toast__undo {
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  font-weight: 500;
  color: var(--color-blue-700);
}

.toast__undo:hover { text-decoration: underline; }
/* Undo and Redo side by side: each is its own button_to form. */
.toast__body form { display: inline-block; margin-right: 1rem; }

.toast__title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0;
  padding: 0.5rem 0.4rem 0.5rem 0.75rem;
  color: var(--color-slate-900);
  font-size: 0.8rem;
  font-weight: 700;
}

.toast__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  padding: 0;
  font: inherit;
  font-weight: 700;
  line-height: 1;
  color: var(--color-slate-600);
  background: transparent;
  border: 0;
  border-radius: 0.4rem;
  cursor: pointer;
}

.toast__close:hover { background: var(--color-slate-100); color: var(--color-slate-900); }

.toast__body {
  margin: 0;
  padding: 0 0.75rem 0.6rem;
  list-style: none;
  font-size: 0.8125rem;
}

.toast__body li + li { margin-top: 0.3rem; }

.toast__link { color: var(--color-accent); }

.toast__where {
  display: block;
  font-size: 0.7rem;
  color: var(--color-slate-400);
}

/* One reminder in the popup: what it is about, and a one-tap way to push it
   later. See reminders/_toast and Reminders::SnoozesController. */
.toast__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}
.toast__snooze-form { flex-shrink: 0; }
.toast__snooze {
  font: inherit;
  font-size: 0.7rem;
  color: var(--color-slate-500);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.toast__snooze:hover { color: var(--color-slate-900); text-decoration: underline; }

/* ---- Status bar ---------------------------------------------------------
 *
 * A slim, quiet strip saying what is true right now, separated by dots
 * rather than sunken panels.
 */

.statusbar {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  /* The bottom of the screen. The foot and the sides pad back by the insets
     so the bar sits above the home bar rather than under it. env() is 0 with
     no insets, so this stays 0.5rem 1rem on a laptop. */
  padding:
    0.5rem
    calc(1rem + env(safe-area-inset-right, 0px))
    calc(0.5rem + env(safe-area-inset-bottom, 0px))
    calc(1rem + env(safe-area-inset-left, 0px));
  background: var(--color-slate-100);
  border-top: 1px solid var(--color-slate-200);
  font-size: 0.75rem;
  color: var(--color-slate-600);
}

.statusbar__panel { display: flex; align-items: center; }

.statusbar__panel--wide { flex: 1; }

.statusbar__panel:not(:last-child)::after {
  content: "·";
  margin-left: 0.9rem;
  color: var(--color-slate-400);
}

/* Light, Dark, Device — the one switch that is a write rather than a fact.
   Quiet on purpose: the accent is already spent on the page-head action,
   and a second filled control here would be a second primary. */
.statusbar__scheme {
  flex-shrink: 0;
  gap: 0;
  border: 1px solid var(--color-slate-200);
  border-radius: 0.5rem;
  overflow: hidden;
}

.statusbar__scheme form { display: flex; margin: 0; }

.statusbar__scheme form + form { border-left: 1px solid var(--color-slate-200); }

.statusbar__scheme button {
  padding: 0.15rem 0.55rem;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--color-slate-600);
  white-space: nowrap;
  cursor: pointer;
}

.statusbar__scheme button:hover { color: var(--color-slate-900); }

.statusbar__scheme button[aria-pressed="true"] {
  background: var(--color-slate-100);
  color: var(--color-slate-900);
  font-weight: 600;
}

@media (max-width: 40rem) {
  .statusbar {
    gap: 0.55rem;
    padding:
      0.45rem
      calc(0.7rem + env(safe-area-inset-right, 0px))
      calc(0.45rem + env(safe-area-inset-bottom, 0px))
      calc(0.7rem + env(safe-area-inset-left, 0px));
  }
  .statusbar__panel:not(:last-child)::after { margin-left: 0.55rem; }
  .statusbar__scheme button { padding: 0.15rem 0.4rem; }
}

/* The review link is the other write in this bar: it opens the dialog
   listing what came back invalid. Accent, because it is the thing to do
   with those rows, not a second colour scheme. */
.statusbar__link {
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--color-accent);
  text-decoration: underline;
  cursor: pointer;
}

/* ---- Scrollbars ---------------------------------------------------------- */

* { scrollbar-color: var(--color-slate-400) transparent; scrollbar-width: thin; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--color-slate-200);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--color-slate-400); }

/* ---- Selection and etched lines ------------------------------------------ */

hr { border: 0; border-top: 1px solid var(--color-slate-200); }

/* ---- The sidebar tree ----------------------------------------------------
 *
 * The template already draws the nesting with a border and indent
 * (border-slate-200, pl-3); no separate tree furniture is needed on top of
 * it, so nothing here overrides it.
 *
 * ---- Held shut until the fold happens --------------------------------------
 *
 * The sidebar is `<details open>` so that it is the sidebar with no
 * JavaScript at all, and `fold_controller` shuts it again on a narrow screen.
 * The trouble is when: first paint lands 20 to 35ms before the module runs,
 * warm cache and all, so the reader on a phone sees the whole tree and then
 * sees it go.
 *
 * This holds it shut for those two frames. The controller removes the class
 * the moment it has decided, and from then on the `open` attribute is the
 * only thing saying anything — so a reader who unfolds it is never fighting
 * this rule.
 *
 * `display: none` rather than anything subtler because the `<noscript>` beside
 * the sidebar reverts exactly this declaration. Its direct children are a
 * search form and a nav, neither of which is given a display of its own; a
 * child that wanted one would need that rule to say more than `revert`.
 */
@media (max-width: 47.999rem) {
  .fold-pending > :not(summary) { display: none; }
}

/* ---- Tabs ------------------------------------------------------------------
 *
 * A tab is quiet until it is the one you are on, which gets an accent
 * underline rather than a raised edge.
 */

main .page-strip nav a,
main .page-strip a {
  padding: 0.375rem 0.8rem;
  color: var(--color-slate-600);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 120ms ease-out, border-color 120ms ease-out;
}

main .page-strip nav a[aria-current],
main .page-strip a[aria-current] {
  color: var(--color-slate-900);
  font-weight: 600;
  border-bottom-color: var(--color-accent);
}

main .page-strip {
  background: transparent;
  margin: 0;
  padding-left: 0;
  padding-right: 0;
  /* One soft line under the tabs, the same line the table draws between its
   * rows. The strip sat over the page on the darker slate-200 rule; the active
   * tab's accent underline is what names the current view, so the rule itself
   * can recede to structure felt, not seen. See docs/design-next.md. */
  border-bottom-color: var(--color-slate-100);
}

/* ---- The pager -------------------------------------------------------------
 *
 * A status bar for a list: where you are, and the two directions out of it.
 */

.pager {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.pager__step {
  padding: 0.35rem 0.85rem;
  font-size: 0.8125rem;
  color: var(--color-slate-900);
  text-decoration: none;
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: 999px;
  transition: color 120ms ease-out, border-color 120ms ease-out;
}

.pager__step:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* A step with nowhere to go is drawn as a dead key rather than removed, so
   the pager does not change width when you reach an end. */
.pager__step--spent {
  color: var(--color-slate-400);
  border-color: var(--color-slate-200);
  background: var(--color-slate-100);
}
.pager__step--spent:hover { color: var(--color-slate-400); border-color: var(--color-slate-200); }

.pager__where {
  padding: 0.35rem 0.6rem;
  font-size: 0.75rem;
  color: var(--color-slate-600);
  font-variant-numeric: tabular-nums;
}

/* ── Between one page and the next ──────────────────────────────────────────
 *
 * What a Turbo visit looks like while it is happening. The page swap itself is
 * instant, and an instant swap reads as a flicker: the eye is told that
 * something changed without being told what. A short cross-fade puts the two
 * states in sequence instead of on top of one another.
 *
 * Deliberately quick. This is furniture, not choreography — anything long
 * enough to notice as an animation is long enough to be in the way of somebody
 * who is working, and every page here is somewhere a person is trying to get
 * something done.
 *
 * Turbo checks prefers-reduced-motion before it starts a transition at all, so
 * this rarely runs for somebody who has asked for less movement. It is said
 * again below anyway: the media query costs three lines and covers the case
 * where a transition is started by something other than Turbo.
 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 150ms;
  animation-timing-function: ease-out;
}

/* The outgoing page leaves without moving. Sliding it would imply a direction,
   and a Turbo visit has none to imply — back and forward look the same here. */
::view-transition-old(root) { animation-name: page-out; }
::view-transition-new(root) { animation-name: page-in; }

@keyframes page-out {
  to { opacity: 0; }
}

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

/* A named group — a row and that row's own page — interpolates size and
   place. Same 150ms as the root fade. The panel does not take the name:
   it sits over the table, and a name may be worn once. See docs/turbo.md. */
::view-transition-group(*) {
  animation-duration: 150ms;
  animation-timing-function: ease-out;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }

  ::view-transition-group(*) {
    animation: none;
  }

  /* The disclosure chevron tweens its rotation over 150ms — the one transform
     this stylesheet transitions, and the one bit of travel not already behind
     a no-preference gate or drawn as a plain fade. Still the tween here: the
     chevron still points the way it points (open or shut is a direction, not
     motion), it just no longer swings to get there. */
  .folder__chevron { transition: none; }
}

/*
 * ---- The palette taken away -----------------------------------------------
 *
 * Windows High Contrast, standardised as `forced-colors: active`, does not ask
 * the app to try harder — it takes the palette away and repaints everything in
 * the reader's own system colours. Every `var(--color-…)` above is overridden,
 * and only what the app drew in a way the OS keeps survives. Most of the app is
 * fine: the base focus ring is an `outline` (kept and repainted), a border is a
 * border, and text is text. What does not survive is state drawn as a
 * `box-shadow` (dropped whole) or as a `background` wash (forced to Canvas) —
 * the same class of problem as a transparent control giving a select's list
 * nothing to draw from, one layer out. This block hands each of those a
 * system colour so the affordance comes back. See docs/design-frontier.md.
 */
@media (forced-colors: active) {
  /* The keyboard's cell-grid ring is an inset box-shadow (a cell clips an
     outline, and the rows scroll sideways), so forced colours drop it and the
     reader driving from the keyboard loses the one mark that says which cell
     they are on. Draw it as an outline here — offset inward so a cell in the
     scrolling frame still shows it — in the system focus colour. */
  /* Drawn only while no field has the keyboard, the same wait as the ring
     above — one mark at a time. */
  :root:not(:has(:is(input, select, textarea, [contenteditable="true"]):focus-visible))
    [data-controller="cell-edit"] td[data-grid-focus] {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }

  /* A selected range is a background wash, and a wash is forced to Canvas, so
     the selection would vanish. Opt this one element out and draw it in the
     system selection colours, which is what a selection is meant to look like
     anyway. */
  [data-controller="cell-edit"] td[data-grid-selected],
  [data-stuck][data-grid-selected] {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }

  /* The drag-into-parent target is a box-shadow ring too. */
  .nest-target > td,
  .nest-target > td:first-child,
  .nest-target > td:last-child {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }

  /* "New since you last looked" is a coloured left edge, and colour is what was
     taken. Keep it as a border so the mark still marks the row. */
  tr.is-new-since > td:first-child,
  .card.is-new-since,
  .list-row.is-new-since {
    border-left: 3px solid Mark;
  }
}

/*
 * The trial: what a half-written formula or condition does to real rows, in a
 * frame under the box that writes it. See app/models/trial.rb and
 * app/javascript/controllers/trial_controller.js.
 */
.trial { display: block; margin-top: 0.75rem; }

.trial__hint,
.trial__caption,
.trial__note { font-size: 0.75rem; color: var(--color-slate-600); }

.trial__caption { margin-bottom: 0.35rem; font-weight: 500; }
.trial__note { margin-top: 0.4rem; color: var(--color-slate-400); }

/* The same refusal red the app uses for a failed save — a trial that can't be
   read says so in the words a save would. */
.trial__error { font-size: 0.75rem; color: var(--color-red-700); }

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

.trial__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.3rem 0.5rem;
  border-radius: 0.375rem;
  background: var(--color-slate-100);
  font-size: 0.8rem;
}

.trial__title {
  min-width: 0;
  overflow: hidden;
  color: var(--color-slate-900);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trial__value {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--color-slate-900);
  white-space: nowrap;
}

.trial__value--yes { color: var(--color-emerald-700); }
.trial__value--no { color: var(--color-slate-400); }

/* A column a dragged card's workflow will not let it reach, greyed while the
 * drag is in flight. Advisory: the enforcement is on the server, and a drop
 * here still bounces back. See board_hints_controller and docs/workflows.md. */
.board-column--blocked { opacity: 0.4; transition: opacity 0.1s; }

/* Workspace list cards grouped the same way the sidebar is: a heading spans
 * the grid, the cards sit under it. A drop that crosses a heading writes the
 * folder. See docs/folders.md. */
.types-board {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: 0.75rem;
}
.types-board__heading {
  grid-column: 1 / -1;
  margin: 0.25rem 0 0;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-slate-400);
}
.types-board > .types-board__heading:first-child { margin-top: 0; }

/* The live-exploration bar: filters that narrow the rows in place, above the
 * table's own frame. Stacked bands, not one wrapping row — search and a
 * sentence sit together, picked filters underneath, sort and the actions
 * on a tools line that can wrap without eating Clear. See
 * live_filter_controller and docs/exploration.md. */
.explore-bar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.625rem;
  margin: 0 0 0.75rem;
  /* A block of its own: the tools for looking sit together on one face,
     rather than loose on the ground above the rows. See "Blocks". */
  padding: 0.875rem 1rem;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  box-shadow: var(--shadow-sm);
}
.explore-bar__label { align-self: flex-start; font-size: 0.75rem; font-weight: 600; color: var(--color-slate-600); }
.explore-bar__find {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.5rem 0.75rem;
}
.explore-bar__find > input[type="search"] {
  flex: 1 1 12rem;
  min-width: 10rem;
}
.explore-bar__rows { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.explore-row[data-complete] .explore-row__edit { display: none; }
.explore-row:not([data-complete]) .explore-row__chip { display: none; }
.explore-row__chip { display: flex; align-items: center; gap: 0.125rem; }
.explore-chip {
  max-width: 24rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border: 1px solid var(--color-slate-200);
  background: var(--color-slate-50);
  border-radius: var(--radius-lg);
  padding: 0.2rem 0.6rem;
  font-size: 0.8125rem;
  color: var(--color-slate-700);
  cursor: pointer;
}
.explore-chip:hover { border-color: var(--color-slate-300); color: var(--color-slate-900); }
/* One-tap filters pinned to a view — see views/_presets. The explore chip's
   face, so the two read as the same kind of thing. The one that is on is
   marked by a darker edge, weight and a tick rather than a fill: a filled
   control beside the page-head action would be a second primary. */
.view-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0 0 0.5rem; }
.view-chip { display: inline-flex; align-items: center; }
.view-chip__link {
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border: 1px solid var(--color-slate-200);
  background: var(--color-slate-50);
  border-radius: var(--radius-lg);
  padding: 0.2rem 0.6rem;
  font-size: 0.8125rem;
  color: var(--color-slate-700);
}
.view-chip__link:hover { border-color: var(--color-slate-300); color: var(--color-slate-900); }
.view-chip__link[aria-current="true"] { border-color: var(--color-slate-500); color: var(--color-slate-900); font-weight: 600; }
.view-chip__remove-form { display: inline-flex; }
.view-chip__remove {
  margin-left: 0.125rem;
  padding: 0.2rem 0.35rem;
  font-size: 0.75rem;
  color: var(--color-slate-400);
  border-radius: var(--radius-md);
}
.view-chip__remove:hover { color: var(--color-slate-900); background: var(--color-slate-100); }
.column-menu__range { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.75rem; }
.column-menu__range label { flex: none; width: 4.5rem; font-size: 0.8125rem; color: var(--color-slate-500); }
.view-chip__edit { position: relative; display: inline-flex; }
.view-chip__edit > summary { list-style: none; cursor: pointer; }
.view-chip__edit > summary::-webkit-details-marker { display: none; }
.view-chip__panel {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 15rem;
  margin-top: 0.25rem;
  padding: 0.625rem;
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.view-chip__rename { display: flex; align-items: center; gap: 0.5rem; }
.view-chip__move { display: flex; gap: 0.5rem; }
.explore-bar__pin { position: relative; }
.explore-bar__pin > summary { list-style: none; cursor: pointer; }
.explore-bar__pin > summary::-webkit-details-marker { display: none; }
.explore-bar__pin-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.explore-bar__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
}
.explore-bar__sort {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
}
.explore-bar__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.explore-bar__actions a { flex-shrink: 0; white-space: nowrap; }
.explore-bar__ask { flex: 1 1 16rem; min-width: 12rem; }
.explore-bar__ask-input { width: 100%; }
.explore-bar__ask-error { margin: 0.25rem 0 0; font-size: 0.75rem; color: var(--color-red-700); }

/* On a phone, the bar folds to one line: the search box and an Explore
 * switch. The switch is a nameless checkbox, so it is never sent and needs no
 * script; `:has()` reads it. Wider than 40rem the switch is not drawn and the
 * bar is always open. While a filter or a sentence hides rows, the switch
 * says how many and Clear stands beside it — a folded bar must never hide a
 * slice. See docs/what-the-screens-say.md §5. */
.explore-bar__open,
.explore-bar__folded-clear { display: none; }
/* The switch's checkbox goes with its label. `sr-only` hides it from the eye
 * and not from the keyboard, so wider than a phone — where the label is not
 * drawn — it was a stop in the tab order with no name and nothing to do. */
.explore-bar__open-box { display: none; }
.explore-bar__filtered { color: var(--color-slate-500); font-weight: 400; }
.explore-bar__filtered::before { content: "·"; margin-inline: 0.35rem 0.25rem; }

@media (max-width: 40rem) {
  .explore-bar__open-box { display: block; }
  .explore-bar__open { display: inline-flex; gap: 0; cursor: pointer; }
  .explore-bar__open-box:focus-visible + .explore-bar__open {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  .explore-bar:not(:has(.explore-bar__open-box:checked)) > .explore-bar__label,
  .explore-bar:not(:has(.explore-bar__open-box:checked)) .explore-bar__ask,
  .explore-bar:not(:has(.explore-bar__open-box:checked)) .explore-bar__rows,
  .explore-bar:not(:has(.explore-bar__open-box:checked)) .explore-bar__tools {
    display: none;
  }

  .explore-bar:not(:has(.explore-bar__open-box:checked)):has(.explore-bar__filtered:not([hidden])) .explore-bar__folded-clear {
    display: inline;
  }

  /* One line: the search box takes what the switch leaves. While a slice is
     on, the switch says only "Filtered · 2" — the word Explore stays for a
     screen reader, and the room goes to the search box and Clear. */
  .explore-bar__find > input[type="search"] { flex-basis: 9rem; min-width: 0; }
  .explore-bar__open:has(.explore-bar__filtered:not([hidden])) .explore-bar__open-word {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .explore-bar__open:has(.explore-bar__filtered:not([hidden])) .explore-bar__filtered::before { content: none; }
}

/* ---- A row of controls -------------------------------------------------
 *
 * A filter, a condition on what a relation offers, one choice of a select,
 * an automation's action: a field, an operator or a word, a value, and the
 * way to remove the row. Written eight times as `flex items-center gap-2`,
 * and wrong on a phone in the same way every time.
 *
 * **A <select> is never narrower than its widest option.** That is the whole
 * of the bug. A field called "Estimated completion date" makes one 240px
 * wide, `flex-shrink` may not touch it, and the row could not wrap — so the
 * value box was pushed off the right-hand edge of a 390px screen (measured
 * at 427px, 26px wide) and `overflow-x: hidden` on <body> took it away
 * rather than offering a sideways scroll. Somebody on a phone could not type
 * a value at all, and nothing said why.
 *
 * So the row wraps, and every control in it may shrink to a floor it is
 * still usable at. `min-width: 0` is what lifts a select's automatic minimum;
 * the 8rem basis is what makes it wrap to its own line instead of shrinking
 * past readable. Nothing changes on a screen wide enough for one row.
 *
 * The button at the end is not matched, so × stays × rather than growing to
 * a quarter of the row.
 */
.control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* A select keeps the width its options ask for while the line has room for
   it, and may now shrink rather than push the row off the screen. */
.control-row > select,
.explore-row > select {
  min-width: 0;
  max-width: 100%;
  flex: 0 1 auto;
}

/* A value box takes what is left, and drops to a line of its own below 8rem
   rather than becoming a box too narrow to read what is typed into it.
 *
 * A box you type in, and nothing else that arrives as an <input>: a submit is
 * a button whatever tag Rails reaches for, and a tick box grown to a third of
 * the row would be a tick box nobody could aim at. */
.control-row > input:not([type="submit"], [type="button"], [type="reset"],
                        [type="checkbox"], [type="radio"]),
.control-row > textarea,
.explore-row > input {
  min-width: 0;
  max-width: 100%;
  flex: 1 1 8rem;
}

.explore-row { flex-wrap: wrap; }

/* The choices a filter's "is any of" picks from, drawn by
 * filter_value_controller in place of the value box. A row of its own under
 * the test, so a long label never squeezes the selects beside it; a place a
 * feed has grown forty spellings of scrolls inside the box rather than
 * pushing the form down a screen. */
.choice-checklist {
  flex: 1 1 100%;
  min-width: 0;
  max-height: 12rem;
  overflow-y: auto;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  padding: 0.375rem 0.5rem;
}
.choice-checklist__search {
  width: 100%;
  margin-bottom: 0.375rem;
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-md);
  padding: 0.25rem 0.5rem;
  font-size: 0.8125rem;
}
.choice-checklist__choices { display: flex; flex-wrap: wrap; gap: 0.25rem 0.875rem; }
.choice-checklist__choice {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 2rem;
  font-size: 0.8125rem;
  color: var(--color-slate-700);
}
.choice-checklist__empty { font-size: 0.8125rem; color: var(--color-slate-500); }

/* A timeline bar is dragged sideways to reschedule its row: the cursor says
 * so, and touch-action:none lets a touch move the bar rather than scroll the
 * page under it. See timeline_drag_controller and docs/timeline.md. */
.timeline-bar { cursor: grab; touch-action: none; }
.timeline-bar:active { cursor: grabbing; }
.timeline-handle { cursor: ew-resize; }

/*
 * Printing a page.
 *
 * docs/gems.md turned down wicked_pdf and grover on the same page it named
 * the free version of this: nothing here writes `@media print` at all, and
 * a print stylesheet gets a PDF of the view somebody actually designed —
 * their layout, their vocabulary, their cell partials — through the
 * browser they already have, rather than a second drawing of it.
 *
 * What prints is the content. Everything that exists to get you somewhere
 * else — the menu bar, the sidebar, the status bar, a page's own strip of
 * tabs and action buttons, the pager, a flash message, every dialog — is
 * furniture for a screen, and none of it means anything on paper.
 */
/* Shown on paper, never on screen — the other half of `.no-print`. A caption a
   view prints so the sheet says which type and slice it is, once the strip that
   said so on screen has been dropped as furniture. See views/show. */
.print-only { display: none; }

@media print {
  .menubar,
  .sidebar,
  .statusbar,
  .page-actions,
  .page-strip,
  .pager,
  .flash,
  #toasts,
  dialog,
  .panel,
  /* Anything a page marks as furniture of its own — the controls a
     dashboard builds itself with, which are not part of what it says on
     paper. See dashboards/show.html.erb, and docs/dashboards.md's note on
     a dashboard printing as a report. */
  .no-print {
    display: none !important;
  }

  .print-only { display: block; }

  body { background: var(--color-white); }

  /* A table's own horizontal scrollbar is exactly what paper doesn't have —
     without this, anything wider than the page is clipped rather than
     wrapped, which is the one way printing a table can lose data rather
     than just look plain. */
  .overflow-x-auto { overflow: visible; }

  /* Paper has no sideways scroll, so a pinned column is just a column. */
  [data-stuck] {
    position: static;
    left: auto;
    box-shadow: none;
  }

  /* A barcode picture is small on screen so a table stays a table. On
     paper it has to be large enough for a phone to read back. See
     docs/barcode.md. */
  .barcode-picture {
    width: 5rem;
    height: 5rem;
  }

  /* A view on paper. Repeat the column headers on every sheet a long table
     runs to, and keep a row — or a card, or a list line — from being split
     across a page break. The columns that only exist to act on a row (the
     tick, the drag handle, the row's own buttons) carry `.no-print` and are
     already gone above. See views/show and docs/printing.md. */
  thead { display: table-header-group; }
  tr, .card, .list-row { break-inside: avoid; }

  .print-caption {
    margin: 0 0 0.75rem;
    font-size: 0.8125rem;
    color: var(--color-slate-500);
  }

  /* One row as a label. In label mode the row's sheet drops everything but the
   * label block, and the block prints alone at the top of the page, sized to a
   * sticker. Outside label mode the block is already hidden (below), so an
   * ordinary print of the row page is untouched. See print_controller#label,
   * items/_label and CON-112. */
  body.is-label .form-sheet > :not(.label) { display: none !important; }
  body.is-label .label { display: block; }
  body.is-label .form-sheet { padding: 0; box-shadow: none; border: 0; }
}

/* The label is only ever a printed thing, never on screen. */
.label { display: none; }
.label__title { margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--color-slate-900); }
.label__code { margin-top: 0.35rem; display: flex; align-items: center; gap: 0.5rem; }
.label__code-text { font-family: ui-monospace, monospace; font-size: 0.85rem; }
.label__fields { margin: 0.5rem 0 0; }
.label__field { display: flex; gap: 0.5rem; font-size: 0.85rem; }
.label__field dt { color: var(--color-slate-500); }
.label__field dd { margin: 0; color: var(--color-slate-900); }
@media print {
  .label {
    width: 3.5in;
    padding: 0.2in;
    border: 1px solid var(--color-slate-300);
    border-radius: 0.15in;
  }
}

/* ---- Reading mode ----------------------------------------------------------
 *
 * The print report, on screen. A reader turns it on to sit with one row and
 * off to act, so it rides the address (?reading=1) rather than a stored
 * preference: Back leaves it, a reload keeps it, and it needs no JavaScript.
 * It hides the same chrome `@media print` does, by the same names, and the
 * row's own `.form-sheet` is already the comfortable measure — so nothing here
 * sets a width, the hidden sidebar just hands the room back. The one addition
 * over print is the explore bar, which paper never draws because a printed
 * view has no rows frame to filter. See docs/reading.md. */
body.is-reading .menubar,
body.is-reading .sidebar,
body.is-reading .statusbar,
body.is-reading .page-actions,
body.is-reading .page-strip,
body.is-reading .explore-bar,
body.is-reading .pager,
body.is-reading .no-print {
  display: none !important;
}

body.is-reading { background: var(--color-white); }

/* The one way out of the mode, floated clear of the sheet. Not on paper: the
 * mode is a screen's, and a printed page has left it by definition. */
.reading-exit {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 50;
}
@media print { .reading-exit { display: none; } }

/* The listing that filled the reading pane. A quiet mark, not a second
 * primary: the close control on the pane is the accent on that screen.
 */
tr[data-reading] > td { background: var(--color-slate-50); }
tr[data-reading] > td:first-child {
  box-shadow: inset 3px 0 0 var(--color-slate-400);
}
article[data-reading] {
  background: var(--color-slate-50);
  box-shadow: inset 3px 0 0 var(--color-slate-400);
}

/* ---- The panel a row opens in -----------------------------------------
 *
 * Beside the list on a wide screen, over it on a narrow one: the rows stay
 * where they were, with their scroll and their filters, and the address
 * says the row — so Back closes this and a reload gives the row's own page.
 * The markup is a turbo-frame the layout carries empty and a link fills by
 * name; see items/panel.html.erb and docs/responding.md.
 *
 * Empty on nearly every page, so it draws nothing at all until it holds
 * something — the same `:empty` rule the toasts use, for the same reason:
 * an invisible fixed overlay over the whole page would eat every click.
 *
 * Under the notifications (z-index 50) and over the menus (40) while it is
 * a sheet. Docked, it sits under the menus so File and View still open over
 * it. A reminder arriving while somebody reads a row still has to be
 * readable.
 */

.panel:not(:empty) {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  overflow-y: auto;
  /* A pull down at the top of the sheet closes it (swipe_controller.js).
     Contained, so the same pull does not also reach the page underneath and
     start the phone's own pull-to-refresh. */
  overscroll-behavior-y: contain;
  /* The row sheet covers the whole screen on a phone, so its own padding is
     what keeps the sheet off the notch and the home bar. env() is 0 with no
     insets, so this stays 1.5rem 1rem 4rem on every other screen. The docked
     variant below sets padding: 0 and is unaffected. */
  padding:
    calc(1.5rem + env(safe-area-inset-top, 0px))
    calc(1rem + env(safe-area-inset-right, 0px))
    calc(4rem + env(safe-area-inset-bottom, 0px))
    calc(1rem + env(safe-area-inset-left, 0px));
  background: var(--color-scrim);
  animation: panel-in 140ms ease-out;
}

/* Closing fades the sheet and its scrim before Back takes them away — see
   panel_controller.js#close. The docked pane below undoes it: it never
   animated in, and a list reflowing to its full width is movement enough. */
@media (prefers-reduced-motion: no-preference) {
  .panel--leaving:not(:empty) {
    opacity: 0;
    pointer-events: none;
    transition: opacity 120ms ease-in;
  }
}

.panel__sheet {
  position: relative;
  width: min(36rem, 100%);
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: 0.75rem;
  box-shadow: var(--shadow-lg);
  padding: 2rem 1.5rem 1.5rem;
}

.panel__actions {
  position: absolute;
  top: 0.5rem;
  right: 0.75rem;
}

.panel__close {
  border: 0;
  background: none;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--color-slate-400);
  padding: 0.25rem 0.4rem;
  border-radius: 0.375rem;
}

.panel__close:hover { color: var(--color-slate-900); background: var(--color-slate-100); }

/* Turn a row page to the next or previous row of the view it was opened from.
 * Quiet furniture above the headline: turning the page must never read louder
 * than the row it lands on, so no accent and no fill. The count is centred and
 * tabular so it does not jitter as the number of digits changes. At the ends a
 * step is dimmed and inert rather than gone, so the control keeps its shape.
 * See Flip and docs/flip.md. */
.flip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1rem;
  font-size: 0.8125rem;
}

/* Beside the panel's own close button, so "Next →" clears the × in the corner. */
.panel__sheet .flip { padding-right: 1.5rem; }

.flip__step { color: var(--color-slate-500); white-space: nowrap; }
.flip__step:hover { color: var(--color-slate-900); }
.flip__step--next { text-align: right; }
.flip__step--off { color: var(--color-slate-300); cursor: default; }

.flip__count {
  color: var(--color-slate-400);
  font-variant-numeric: tabular-nums;
}

/* The same query as DOCKED in panel_controller.js. A foldable inner
 * screen is about 40–53rem wide and nearly square — it never reaches
 * the 64rem laptop the pane used to wait for. Height is the other
 * half: a phone on its side is wide enough and not tall enough, and
 * a sheet over a scrim is still the right drawing there.
 *
 * Two viewport segments (a book-posture fold, a dual screen) dock
 * too. The hinge block below puts the pane in the second segment.
 *
 * The reserved strip on the menubar, the page and the status bar is
 * the pane's own width, so the list is not drawn under it. The pane
 * shrinks on a medium window so the list keeps a column.
 *
 * Held equal to the JS string by test/foldable_queries_test.rb.
 */
@media (min-width: 40rem) and (min-height: 36rem), (horizontal-viewport-segments: 2) {
  body:has(#row_panel:not(:empty)) {
    --reading-pane: clamp(18rem, 38vw, 26rem);
  }

  body:has(#row_panel:not(:empty)) .menubar,
  body:has(#row_panel:not(:empty)) .app-frame,
  body:has(#row_panel:not(:empty)) .statusbar {
    padding-right: var(--reading-pane);
  }

  .panel:not(:empty) {
    inset: 0 0 0 auto;
    width: var(--reading-pane, 26rem);
    justify-content: stretch;
    align-items: stretch;
    padding: 0;
    background: none;
    overflow: hidden;
    animation: none;
    opacity: 1;
    transition: none;
    z-index: 35;
  }

  .panel__sheet {
    width: 100%;
    max-height: 100%;
    border-radius: 0;
    border-width: 0 0 0 1px;
    box-shadow: none;
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }
}

/* A pane plus the sidebar column is what made 64rem the old floor.
 * On a medium window the sidebar goes back to the disclosure it is
 * on a phone, so the list keeps the leftover width. The tree is
 * still one tap away. fold_controller shuts it when the pane arrives.
 */
@media (min-width: 40rem) and (min-height: 36rem) and (max-width: 63.999rem) {
  /* The sidebar folds to a disclosure whenever the leftover width is spoken
     for: a pane is open, or the reader keeps a detail column beside the list
     (data-detail-pane). Either way the list needs the room the column takes —
     16rem of sidebar plus an 18rem pane leaves nothing to read on an inner
     screen — so the two are alternatives here, not companions. */
  :is(body:has(#row_panel:not(:empty)), body[data-detail-pane]) .app-frame {
    display: block;
  }

  :is(body:has(#row_panel:not(:empty)), body[data-detail-pane]) .sidebar {
    position: static;
    top: auto;
    z-index: auto;
    width: auto;
    height: auto;
    overflow: visible;
    border-right-width: 0;
    border-bottom-width: 1px;
  }

  :is(body:has(#row_panel:not(:empty)), body[data-detail-pane]) .sidebar > summary {
    cursor: pointer;
  }

  :is(body:has(#row_panel:not(:empty)), body[data-detail-pane]) .sidebar > summary .float-right {
    display: inline-block;
  }
}

/* The foldable inner screen is a tablet, not a big phone — about 40–48rem and
 * taller than a phone on its side. Below 48rem the `md:` rules that draw the
 * sidebar as a column are off, so it fell back to the folded disclosure a
 * phone gets, and the inner screen navigated like a phone. When the reading
 * pane is closed it now gets the same column a desk gets at 48rem.
 *
 * Not when the reader keeps a detail column open (data-detail-pane): that
 * column is the sidebar's alternative on this screen, and the block above
 * folds the tree so the list keeps its width. So this is the sidebar the inner
 * screen gets when the detail column is off, which is the default.
 *
 * The pane rules never both apply: this asks for no pane, the medium-window
 * block asks for a pane, so opening the pane hands the column to the list and
 * closing it gives the sidebar back. A phone on its side is wide enough to
 * reach 40rem but too short to reach 36rem, so it never matches and keeps the
 * disclosure — it does not meet the whole tree first. Held by
 * test/system/foldable_layout_test.rb.
 */
@media (min-width: 40rem) and (min-height: 36rem) and (max-width: 47.999rem) {
  body:not([data-detail-pane]):not(:has(#row_panel:not(:empty))) .app-frame {
    display: flex;
  }

  body:not([data-detail-pane]):not(:has(#row_panel:not(:empty))) .sidebar {
    position: sticky;
    top: 0;
    z-index: 30;
    height: 100vh;
    width: 16rem;
    flex-shrink: 0;
    overflow-y: auto;
    border-bottom-width: 0;
    border-right: 1px solid var(--color-slate-200);
  }

  body:not([data-detail-pane]):not(:has(#row_panel:not(:empty))) .sidebar > summary {
    cursor: default;
  }

  body:not([data-detail-pane]):not(:has(#row_panel:not(:empty))) .sidebar > summary .float-right {
    display: none;
  }
}

/* The detail column, kept open beside the list when the reader asked for it
 * (users.detail_pane, drawn as `data-detail-pane` on a list page) on a window
 * that can dock. It stands empty until a row is opened — a placeholder in the
 * strip the docked pane will fill — so the two-column layout is there before
 * the first click rather than a click away. The opened pane docks over it and
 * `:not(:has(...))` hands the strip back on close. It is a sibling of
 * `#row_panel`, never its content, so the `:empty` signal keeps its one
 * meaning. On any window that can dock: a desk keeps its sidebar beside the
 * column, a medium window folds it. See docs/detail-by-default.md.
 */
.panel-default { display: none; }

/* A turbo-frame is inline by default; the detail row inside the column has to
   lay out as a block and fill it. */
.panel-default__frame { display: block; }

/* The invitation when the list is empty — or the moment before the lazy row
   arrives. Centred in the column; the row that replaces it fills it instead. */
.panel-default__hint {
  margin: auto;
  padding: 2rem 1.5rem;
  text-align: center;
  color: var(--color-slate-500);
  font-size: 0.9375rem;
  line-height: 1.5;
}

/* The first row, drawn to fill the column rather than to float in it: no card
   edge of its own, because the column already carries the border and the face.
   It is the docked panel's sheet without the raised-panel furniture. */
.panel__sheet--default {
  width: 100%;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* The detail column stands beside the list on any window that can dock, not
 * only the medium one it started on. A desk (64rem and up) has room for all
 * three — the sidebar keeps its column here, because the fold rule that hands
 * it away for the list is medium-only, so a desk reads sidebar, list and
 * detail at once, the way a mail client does. On the medium window the sidebar
 * folds and it is list and detail. See docs/detail-by-default.md. */
@media (min-width: 40rem) and (min-height: 36rem) {
  body[data-detail-pane]:not(:has(#row_panel:not(:empty))) {
    --reading-pane: clamp(18rem, 38vw, 26rem);
  }

  body[data-detail-pane]:not(:has(#row_panel:not(:empty))) .menubar,
  body[data-detail-pane]:not(:has(#row_panel:not(:empty))) .app-frame,
  body[data-detail-pane]:not(:has(#row_panel:not(:empty))) .statusbar {
    padding-right: var(--reading-pane);
  }

  body[data-detail-pane]:not(:has(#row_panel:not(:empty))) .panel-default {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset: 0 0 0 auto;
    width: var(--reading-pane);
    overflow-y: auto;
    border-left: 1px solid var(--color-slate-200);
    background: var(--color-white);
    z-index: 35;
  }
}

/* Book posture / a dual screen: the hinge is the split. The list
 * stays in the first segment; the pane fills the second. Browsers
 * that do not report segments never enter this block.
 */
@media (horizontal-viewport-segments: 2) {
  body:has(#row_panel:not(:empty)) {
    --reading-pane: env(viewport-segment-width 1 0);
  }

  body:has(#row_panel:not(:empty)) .menubar,
  body:has(#row_panel:not(:empty)) .app-frame,
  body:has(#row_panel:not(:empty)) .statusbar {
    box-sizing: border-box;
    width: env(viewport-segment-width 0 0);
    max-width: env(viewport-segment-width 0 0);
    padding-right: 0;
  }

  .panel:not(:empty) {
    top: env(viewport-segment-top 1 0);
    right: auto;
    bottom: auto;
    left: env(viewport-segment-left 1 0);
    width: env(viewport-segment-width 1 0);
    height: env(viewport-segment-height 1 0);
  }

  /* A centred overlay lands on the crease in book posture: the hinge runs down
     the middle of the search box you type in and between the two buttons of a
     confirmation. So each is kept inside the first panel — its box spans that
     segment and it centres within it, the way it centred in the viewport
     before. The reading pane fills the second segment (above); an overlay is
     raised over the whole page, so it goes to the segment the reader's eye is
     already in. Vertical placement is untouched: the confirm still centres, the
     palette still sits below the top. Only ever narrower than the viewport, so
     a browser that misreads the segments draws a smaller centred box, never a
     broken one. */
  .confirm,
  #command-palette,
  #outbox-review {
    left: env(viewport-segment-left 0 0);
    right: calc(100vw - env(viewport-segment-left 0 0) - env(viewport-segment-width 0 0));
    margin-inline: auto;
    max-width: calc(env(viewport-segment-width 0 0) - 2rem);
  }
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes panel-in {
    from { opacity: 0; }
  }
}

/* ---- A table on a phone ------------------------------------------------
 *
 * A twelve-column table on a 375px screen is a box you drag left and right,
 * reading one column at a time and losing which row you were on. The app
 * already knows how to draw the same rows as cards, but that is an
 * *arrangement* — something somebody configured — and a screen being narrow
 * is not a reason to rewrite what they configured. docs/responding.md is
 * explicit: a fallback at a width, not a change to the view, and nothing
 * written to `config`.
 *
 * So the same rows, drawn differently. Each cell stacks under the last and
 * prints its own column's name, which the row carries as `data-label`.
 * Nothing is rendered twice, nothing is hidden and re-drawn, and everything
 * attached to the table keeps working — inline editing, the batch
 * checkboxes, the blank row at the foot — because it is the same DOM.
 *
 * The two columns that are gestures rather than values go: a drag handle is
 * meaningless where dragging scrolls the page, and the header row has
 * nothing left to head.
 */

@media (max-width: 40rem) {
  .table-stack table,
  .table-stack tbody,
  .table-stack tr,
  .table-stack td {
    display: block;
    width: 100%;
  }

  .table-stack thead,
  .table-stack td[data-sortable-handle] {
    display: none;
  }

  .table-stack tr {
    position: relative;
    border-bottom: 1px solid var(--color-slate-200);
    padding: 0.25rem 0.75rem 0.75rem;
  }

  .table-stack td {
    display: flex;
    gap: 0.75rem;
    align-items: baseline;
    padding: 0.35rem 0;
    border: 0;
  }

  /* The column's own name, in front of the value it belongs to. */
  .table-stack td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 8rem;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-slate-500);
  }

  /* The selection box and the row's own actions read as furniture rather
     than as values, so they sit apart from the labelled cells. */
  .table-stack td:not([data-label]) {
    justify-content: flex-end;
    padding-top: 0.5rem;
  }

  .table-stack tfoot td:not([data-label]) { display: none; }

  /* Stacked rows have no sideways scroll, so a pin is nothing to keep. */
  .table-stack [data-stuck] {
    position: static;
    left: auto;
    box-shadow: none;
  }

  .fill-handle { display: none; }
}

/* ---- A table in a narrow list on a wide screen ------------------------
 *
 * The rule above reads the *window*. A list can be narrow in a wide window —
 * at 720px with a row open, the reading pane docks and the rows keep about
 * 370px — and there the table stayed a wide table in a sideways scroller,
 * with the headings cut to a letter. So the same rules again, asked of the
 * width the rows actually have. docs/what-the-screens-say.md §1.
 *
 * **Only above 40rem.** The first container queries also applied layout
 * containment, which makes the container the box a `position: fixed`
 * descendant is placed against — and on a phone the hover preview and the
 * hint are fixed sheets at the foot of the *screen*, which would have landed
 * at the foot of the table. The rule was taken back out of the spec, and
 * Chrome no longer does it (checked: a fixed box in a cell still sits at the
 * top of the screen). Below 40rem the window rule above is already true, so
 * there is no reason for a phone to depend on which engine it has.
 *
 * Nothing else is set on the container. A `transform` here *would* be a
 * containing block, in every engine, and would move every preview.
 *
 * The body of the `@container` block is the phone block, character for
 * character. `test/list_width_test.rb` holds them together, because CSS
 * has no way to write one set of rules under two conditions.
 */

@media (width > 40rem) {
  .table-stack { container: rows / inline-size; }
}

@container rows (max-width: 40rem) {
  .table-stack table,
  .table-stack tbody,
  .table-stack tr,
  .table-stack td {
    display: block;
    width: 100%;
  }

  .table-stack thead,
  .table-stack td[data-sortable-handle] {
    display: none;
  }

  .table-stack tr {
    position: relative;
    border-bottom: 1px solid var(--color-slate-200);
    padding: 0.25rem 0.75rem 0.75rem;
  }

  .table-stack td {
    display: flex;
    gap: 0.75rem;
    align-items: baseline;
    padding: 0.35rem 0;
    border: 0;
  }

  /* The column's own name, in front of the value it belongs to. */
  .table-stack td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 8rem;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-slate-500);
  }

  /* The selection box and the row's own actions read as furniture rather
     than as values, so they sit apart from the labelled cells. */
  .table-stack td:not([data-label]) {
    justify-content: flex-end;
    padding-top: 0.5rem;
  }

  .table-stack tfoot td:not([data-label]) { display: none; }

  /* Stacked rows have no sideways scroll, so a pin is nothing to keep. */
  .table-stack [data-stuck] {
    position: static;
    left: auto;
    box-shadow: none;
  }

  .fill-handle { display: none; }
}

/* A number column tinted pale-to-deep by value — four steps of the theme's own
 * amber, so the shade turns over with the scheme rather than staying light on a
 * dark page. The value never becomes a colour written into the markup: the row
 * carries a step class and the shade lives here, read from a token the theme
 * declares. Ink stays the cell's own dark slate, readable on every step. See
 * FieldsHelper#colour_scale_class and docs/colour-scale.md. */
.cell-scale-1 { background-color: var(--color-amber-50); }
.cell-scale-2 { background-color: var(--color-amber-100); }
.cell-scale-3 { background-color: var(--color-amber-200); }
.cell-scale-4 { background-color: var(--color-amber-300); }

/* A data bar behind a number: the same reading of the value as the tint, drawn
 * as a width the eye can compare down the column rather than a shade. The bar
 * is an inline SVG the width of the cell, its fill a `<rect>` whose own width
 * says the fraction — an attribute, never an inline style, so a table of them
 * stays inside the CSP's handful of inline styles. Neutral, not the accent: a
 * bar in every barred cell would spend the one accent everywhere. The value
 * sits above it on its own stacking, ink unchanged and readable. See
 * FieldsHelper#data_bar and docs/data-bars.md. */
/* The bar needs a positioned cell to sit inside. A frozen column already has
 * one — `[data-stuck]` is `position: sticky` — so this is asked for only when
 * the cell is not stuck, or it would come later in the file and undo the
 * freeze. See the sticky-column rules above. */
.cell-barred:not([data-stuck]) { position: relative; }
.cell-bar {
  position: absolute;
  inset: 0.5rem 0;
  z-index: 0;
  pointer-events: none;
}
.cell-value { position: relative; z-index: 1; }

/* ---- A finger is not a pointer ---------------------------------------------
 *
 * The controls are sized for a mouse: a button is 36px tall, a field the same,
 * and the icon-only ones (a close ×, a menu, a page step) are smaller than
 * either. A finger needs about 44px to land on a thing without missing it or
 * hitting its neighbour — the size Apple and Google both name — so on a screen
 * driven by touch every control grows to that floor.
 *
 * `@media (pointer: coarse)` is the whole of the guard: it is true on a phone,
 * a tablet and a foldable, and false on a laptop with a mouse — so nothing
 * here touches the desktop the rest of the app was measured on, and a hybrid
 * laptop with a touch screen keeps its mouse drawing until the mouse is the
 * coarse pointer. It raises a floor with min-height and never a fixed height,
 * so a control that was already taller keeps its size.
 *
 * `btn` and `field` are the hooks the two helpers carry (ButtonsHelper,
 * FormsHelper), so every button and every field is reached by one selector
 * each. The rest are the icon-only controls that do not flow through a helper.
 * The column header menu is not here on purpose: a phone stacks the table and
 * drops the header row, so there is nothing to tap. */
@media (pointer: coarse) {
  .btn,
  .field,
  .menu > summary,
  .menubar__find,
  .pager__step,
  .statusbar__scheme button,
  .panel__close,
  .toast__close,
  .landing__close,
  .view-chip__link,
  .view-chip__remove {
    min-height: 2.75rem;
  }

  /* The icon-only ones carry no text to give them height, so they also need a
     width to land on and their mark centred in it. A menu summary and the
     find box already lay their children out, so they are left to the height
     alone above. */
  .pager__step,
  .panel__close,
  .toast__close,
  .landing__close,
  .view-chip__remove {
    min-width: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* A tick box is 13px by default, which is a hard thing to hit and an easy
     thing to hit by accident. The batch bar's select-all and every row's own
     tick are how a phone acts on more than one row at once, so they are worth
     the room. */
  input[type="checkbox"],
  input[type="radio"] {
    width: 1.4rem;
    height: 1.4rem;
  }

  /* A field smaller than 16px makes iOS Safari zoom the page the moment it
     takes focus, and the first thing that happens is the form is pushed off
     the side of the screen and the reader has to pinch back out. 16px is the
     number that stops it — the same reason the sign-in card already sets it,
     see `.auth-card` — and it is no larger than a field is on a desk; it only
     ever grows the touch drawing. Android Chromium never zoomed, so this costs
     it nothing and a 16px field reads no worse there. Not a tick box, a slider
     or a colour well: nothing you type into, nothing that zooms. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
  select,
  textarea {
    font-size: 16px;
  }
}

/* ---- A dashboard is a desk, not a column -------------------------------
 *
 * Equal cards, as many across as fit, in the order the widgets already keep.
 * No widget has a size, so nothing new is stored, permitted or dragged. `min()`
 * so a card never asks for more than a phone has. On paper, one column: a
 * report reads down the page. See docs/what-the-screens-say.md §6. */
.dashboard-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr));
  align-items: start;
}

@media print {
  .dashboard-grid { display: block; }
  .dashboard-grid > * + * { margin-top: 1rem; }
}
