/*
 * 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. `overflow-x: hidden` here is what keeps a panel's
 * off-screen space from ever becoming the page's problem.
 */
html,
body {
  overflow-x: hidden;
}

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

/*
 * 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: 72rem;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 4rem;
}

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

.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: 0.75rem;
  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: 1.5rem;
  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: 1.25rem; }

/*
 * 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: 26rem;
  margin-top: 1.5rem;
  padding: 1.75rem;
  background: var(--color-white);
  border: 1px solid var(--color-slate-200);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
}

.auth-page .page-body { padding-top: 0; }

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

.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;
  }
}

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

.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;
}

.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;
}

.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;
}

.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__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);
}

/* 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)) { 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)) + .calendar__nothing {
    display: block;
    padding: 1.25rem 0.75rem;
    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);
}
.card--compact { padding: 0.25rem 0.375rem; font-size: 0.75rem; border-radius: 0.25rem; }
.card--compact .footer, .card--compact .bodytext { display: none; }

.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 type's own initial, on the same gradient as the brand mark — one small
   splash of colour on a tile that would otherwise be all text, and a free
   way to tell two type 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;
}

/*
 * 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 {
  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: 28rem; }

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

.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 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; }
}

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

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

/* ---- Hints and previews keep the same warm tooltip as the rest of the app,
   pulled from the theme rather than hardcoded, so it can't drift from it. */

.hint-bubble {
  background: var(--color-amber-50) !important;
  border: 1px solid var(--color-amber-800);
  border-radius: 0.5rem !important;
}

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

/* ---- Form pages -------------------------------------------------------
 *
 * Every new/edit page reaches for the same card wrapper verbatim —
 * `mx-auto max-w-lg rounded-lg border border-slate-200 bg-white px-6 py-8` —
 * so it is qualified here once rather than edited in a dozen templates, the
 * same reasoning as the primary-button override below. A border alone reads
 * flatter here than the menu and toast surfaces sitting on the same paper;
 * the lift is what a floating card gets everywhere else in the app.
 */
main > div.max-w-lg.rounded-lg.border.border-slate-200.bg-white {
  box-shadow: var(--shadow-sm);
}

/* ---- 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;
  padding: 0.6rem 1rem;
  background: var(--color-white);
  border-bottom: 1px solid var(--color-slate-200);
}

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

.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 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 {
  right: 0;
  left: auto;
  width: 22rem;
  max-width: calc(100vw - 1.5rem);
  max-height: 70vh;
  overflow-y: auto;
}

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

/* ---- 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 the one `column_menu_controller` sets is
 * the only one in force.
 *
 * 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.
 */
.column-menu { position: relative; display: inline-block; }

.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; }

.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 { margin: 0; padding: 0; list-style: none; }

.activity__entry {
  display: flex;
  gap: 0.6rem;
  padding: 0.5rem 0.65rem;
  border-radius: 0.5rem;
}

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

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

/* Overrides the block-level menu item above: an entry's link is one line
 * inside three, not the whole row. */
.menu__items--wide .activity__detail a {
  display: inline;
  width: auto;
  padding: 0;
  border-radius: 0;
}

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

.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. */
  top: 3.25rem;
  right: 0.75rem;
  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); }

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

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

/* ---- 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;
  align-items: center;
  gap: 0.9rem;
  padding: 0.5rem 1rem;
  background: var(--color-white);
  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);
}

/* The one thing in the status bar that does something rather than just says
   something — opens the dialog listing what came back invalid. */
.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.5rem 0.85rem;
  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; }

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

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

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

/*
 * 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.
 */
@media print {
  .menubar,
  .sidebar,
  .statusbar,
  .page-actions,
  .page-strip,
  .pager,
  .flash,
  #toasts,
  dialog {
    display: none !important;
  }

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

/* ---- The panel a row opens in -----------------------------------------
 *
 * Over the table rather than instead of it: 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). 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;
  padding: 1.5rem 1rem 4rem;
  background: var(--color-scrim);
  animation: panel-in 140ms ease-out;
}

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

@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; }
}
