/* Blue North CRM — project stylesheet
 *
 * Everything here is what Bootstrap's Sass variables cannot express. Brand
 * colours, radii, shadows and the type scale come from .wappler/theme.json and
 * are compiled into /bootstrap/5/css/bootstrap.min.css — see CLAUDE.md.
 *
 * This file loads AFTER Bootstrap, so a `--bs-*` declaration on :root would
 * silently outrank the theme build and break the Theme tab. Don't add any.
 * The component-scoped `--bs-btn-*` / `--bs-card-*` overrides below are safe:
 * they are Bootstrap's own documented per-component API, not theme values.
 *
 * THE ONE EXEMPTION IS THE DARK BLOCK AT THE FOOT OF THIS FILE. Wappler rejects
 * every `$*-dark` Sass variable (see CLAUDE.md → Theme), so the Theme tab cannot
 * reach dark mode at all and the only place its surfaces can live is here. That
 * block declares `--bs-*` under `[data-bs-theme="dark"]` and nowhere else, so it
 * cannot outrank the light theme build — light mode is still entirely the Sass
 * compiler's, which is what keeps the Theme tab working.
 *
 * `line-height-base` and `border-radius-xl` are rejected outright by this
 * Wappler version, so the only place they can live is here.
 */

/* ---------------------------------------------------------------------------
 * Fonts — Anthropic Sans
 *
 * Self-hosted: it isn't a Google font, so it can't go through theme.json's
 * `headings-font-family` (that key always emits a Google Fonts @import, which
 * would just 404). @font-face lives here instead, and the body/heading rules
 * below set the actual `font-family` property — not a `--bs-*` custom
 * property, so the "don't override those" rule above doesn't apply and the
 * Theme tab's own colour/radius/shadow values are untouched.
 *
 * Two families, matching how Anthropic itself pairs them: Text for body copy,
 * Display for headings.
 * ------------------------------------------------------------------------ */

@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Light-Static.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Regular-Static.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-RegularItalic-Static.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Medium-Static.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-MediumItalic-Static.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Semibold-Static.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Bold-Static.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-BoldItalic-Static.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-Extrabold-Static.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Text";
  src: url("/fonts/anthropic-sans/AnthropicSans-Text-ExtraboldItalic-Static.woff2") format("woff2");
  font-weight: 800;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Light-Static.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Regular-Static.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-RegularItalic-Static.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Medium-Static.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-MediumItalic-Static.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Semibold-Static.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Bold-Static.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-BoldItalic-Static.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-Extrabold-Static.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Anthropic Sans Display";
  src: url("/fonts/anthropic-sans/AnthropicSans-Display-ExtraboldItalic-Static.woff2") format("woff2");
  font-weight: 800;
  font-style: italic;
  font-display: swap;
}

/* ---------------------------------------------------------------------------
 * Tokens
 * ------------------------------------------------------------------------ */

:root {
  --bn-sidebar-w: 264px;
  --bn-topbar-h: 64px;

  /* HOW LONG THE SIDEBAR TAKES TO FOLD, and on what curve. One pair of tokens
     because six things move on that gesture — the panel's own width, the
     shell sliding across to meet it, the section headings' height, each link's
     padding, and two cross-fades — and every one of them has to finish on the
     same frame or the panel arrives before its contents do.

     The curve is fast out of the gate and long on the settle, which is what
     makes a panel read as being pushed rather than tweened; a symmetric ease
     at this distance looks like it is being dragged. See "Folding the rail"
     below for why animating this costs one relayout rather than one per
     frame. */
  --bn-nav-dur: 300ms;
  --bn-nav-ease: cubic-bezier(0.32, 0.72, 0, 1);

  /* The page gutter, and the rhythm everything else is a multiple of.
     Widened from 1.5rem in the design refresh: the shell is the outermost
     margin in the app and every list, board and record page reads as tighter
     than it is when the content starts 24px from the sidebar. */
  --bn-gutter: 2rem;

  /* The vertical step between two blocks on a page — a page head and the
     tiles under it, the tiles and the tabs, one card and the next. Named
     rather than repeated so "more air" is one edit, not forty. */
  --bn-stack-lg: 1.5rem;
  --bn-stack: 0.75rem;
  --bn-stack-sm: 0.75rem;

  /* Card corners. `border-radius-xl` is rejected by Wappler. */
  --bn-radius-card: 1rem;
  --bn-radius: 1rem;
  --bn-radius-sm: 0.5rem;

  /* A LABEL-SHAPED THING IS NOT A STADIUM. Status pills and board chips were
     999px, which at 22px tall is a capsule — the loudest shape available for
     the quietest fact on a row, and the one shape nothing else in the app has
     any more now the theme switch, the tiles and the queue rows all sit on
     small radii. A dot stays a dot and a count badge stays a circle: those are
     round because they are round things, not because they are labels. */
  --bn-radius-pill: 0.375rem;

  /* Interior padding of a surface. One value, so a card, a panel and a table
     cell agree about where their content starts. */
  --bn-pad: 1.2rem;
  --bn-pad-sm: 1rem;

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

     THE TWO NAMES EXIST BECAUSE DARK MODE NEEDS THEM TO MOVE IN OPPOSITE
     DIRECTIONS. `--bs-secondary-bg` was doing two unrelated jobs: the page
     canvas behind everything, and the recess *inside* a surface — a table
     header, a board column head, the search field, a row hover. In light mode
     one tone can be both, because a recess and a canvas are each just "slightly
     less white than a card". In dark mode a canvas has to go DARKER than the
     card and a recess has to go LIGHTER, so a single token cannot serve both
     and the canvas is named separately here.

     `--bn-surface` derives from `--bs-body-bg` rather than restating `#fff`:
     body-bg is Bootstrap's own raised surface — cards, modals, dropdowns and
     form controls all read it — and this app paints the canvas on <body>
     instead, so body-bg is left meaning exactly "a raised surface" in both
     modes. Nothing here duplicates a theme value; both derive. */
  --bn-surface: var(--bs-body-bg);
  --bn-canvas: var(--bs-secondary-bg);

  /* A surface under the pointer. "Lighter on hover" is only literally possible
     in dark mode — in light mode the surface is already white — so what the
     token actually holds is a LIFT: the faintest tint of primary mixed into the
     surface itself, which reads as lit in light mode and is genuinely lighter in
     dark. Primary rather than a neutral because the border and the corner arrow
     already go primary on the same hover; three things moving toward one colour
     is one gesture, three toward two colours is a smudge.

     Opaque, not the transparent `--bn-wash` further down that does the same job
     for a drop target and a chart band: this one is transitioned, and animating
     `background-color` between two flat colours is what makes that cheap. */
  --bn-surface-hover: color-mix(in srgb, var(--bs-primary) 5%, var(--bn-surface));
  --bn-card-hover: color-mix(in srgb, var(--bs-primary) 3%, var(--bs-tertiary-bg));

  /* The sidebar and topbar are chrome, not content — in light mode that is
     still just the surface (a white panel, same as a card), but dark mode
     gives chrome its own tone distinct from both the canvas and a card, so
     the two need to be able to move independently. */
  --bn-chrome: var(--bn-surface);
  --bn-border-hover: #dcdce0;

  /* SCROLLBARS. Two tokens and nothing else: a thumb, and the same thumb under
     a pointer. There is no track token because a track is not drawn at all —
     every scrollbar in the app is a thumb floating over whatever it is
     scrolling. A scrollbar is chrome about chrome; it should say "there is
     more" and then get out of the way, and a full-contrast Chrome default
     scrollbar sitting down the side of a 320px board column says it much
     louder than the content it is describing. Both are a mix of the app's own
     ink into the surface rather than a flat grey, so they follow the theme and
     dark mode is two numbers rather than two hexes. */
  --bn-scroll-thumb: color-mix(in srgb, var(--bs-emphasis-color) 18%, transparent);
  --bn-scroll-thumb-hover: color-mix(in srgb, var(--bs-emphasis-color) 34%, transparent);

  /* How tall a board column may grow before its own cards start scrolling
     inside it. One token because it is a judgement rather than a measurement:
     the columns sit well down the page under a head, the tiles and the filter
     bar, so a height budgeted off the viewport alone always under-counts what
     is on screen. The 2rem is the gutter under the board and nothing else —
     the column is deliberately allowed to run past the fold, because the page
     scrolls and reaching a card by scrolling the PAGE is better than reaching
     it by scrolling a 320px box. */
  --bn-board-col-max: calc(100vh - var(--bn-topbar-h) - 2rem);

  /* How far the fade at a scrolling edge reaches. See the mask block under
     Kanban. */
  --bn-fade: 2rem;

  /* CHROME UNDER A POINTER, AND CHROME THAT IS SELECTED. The same recipe as
     --bn-surface-hover above, mixed into --bn-chrome instead of --bn-surface
     so the sidebar's own tone is what gets tinted — which in light mode is the
     same white and in dark mode is not.

     They exist because the sidebar used to answer a pointer with
     `--bs-secondary-bg` and mark the current page with `--bs-tertiary-bg`: two
     flat greys, and the only two places in the app where a hover or a
     selection is a NEUTRAL. Everything else already speaks primary — a tile
     goes primary-border and primary-tint on hover, a queue row takes
     --bn-surface-hover, the queue's own group heading turns primary, the board
     card's drop target is a primary wash. The navigation, which is the control
     surface you look at most, was the one thing not saying it.

     Two steps of one hue rather than one step of two hues: 6% is a pointer
     resting somewhere, 12% is where you actually are. The active step carries
     primary INK as well, which is what makes it readable as a state rather
     than as a slightly darker rectangle. */
  --bn-nav-hover: color-mix(in srgb, var(--bs-primary) 6%, var(--bn-chrome));
  --bn-nav-active: color-mix(in srgb, var(--bs-primary) 12%, var(--bn-chrome));


  /* An inverted fill — the current stage pill, the active bucket chip, the
     chart tooltip. This CANNOT be `--bs-emphasis-color` with white on it, which
     is what it used to be: emphasis-color is #fff in dark mode, so all three
     became white text on a white box. The pair inverts together instead. */
  --bn-invert-bg: var(--bs-emphasis-color);
  --bn-invert-fg: #fff;
  --bn-invert-fg-dim: rgba(255, 255, 255, 0.72);

  /* The brand square. `--bs-dark` is a theme colour and does not follow the
     mode, so in dark mode it lands within a shade of the surface it sits on. */
  --bn-mark-bg: var(--bs-dark);
  --bn-mark-fg: #fff;

  /* Shadows. Softer and shorter-throw than Bootstrap's, because almost every
     surface here is a hairline-bordered white block on a near-white canvas —
     at that contrast a visible drop shadow reads as grime rather than lift. */
  --bn-lift: 0 1px 2px rgba(24, 24, 27, 0.04);
  --bn-lift-lg: 0 1px 2px rgba(24, 24, 27, 0.04), 0 12px 28px -10px rgba(24, 24, 27, 0.12);

  /* -------------------------------------------------------------------------
     Status tints — a wash of a theme colour, and the ink that goes on it.

     These were rgba() literals holding the theme's own RGB triplets, at four
     different alphas that had drifted apart (0.1 and 0.12 for the same hue in
     different places). They are one alpha and one `color-mix` now, so a colour
     retuned in the Theme tab carries through to every pill and chip instead of
     leaving the wash pointing at the old hue.

     The ink is NOT `--bs-*-text-emphasis`. Bootstrap derives that as a 60%
     shade, which is markedly darker than the tones the app was using; adopting
     it would restyle every pill in light mode. The light values below are the
     app's own, unchanged. Dark mode DOES hand over to Bootstrap's dark
     `*-text-emphasis`, because there the Sass-derived tint is exactly right and
     following it keeps the ink in step with the Theme tab. */
  --bn-fill-primary: color-mix(in srgb, var(--bs-primary) 12%, transparent);
  --bn-fill-success: color-mix(in srgb, var(--bs-success) 12%, transparent);
  --bn-fill-warning: color-mix(in srgb, var(--bs-warning) 12%, transparent);
  --bn-fill-danger:  color-mix(in srgb, var(--bs-danger)  12%, transparent);
  --bn-fill-info:    color-mix(in srgb, var(--bs-info)    12%, transparent);

  --bn-edge-success: color-mix(in srgb, var(--bs-success) 35%, transparent);
  --bn-edge-warning: color-mix(in srgb, var(--bs-warning) 35%, transparent);
  --bn-edge-danger:  color-mix(in srgb, var(--bs-danger)  35%, transparent);

  --bn-on-primary: #4338ca;
  --bn-on-success: #AEE362;
  --bn-on-warning: #FFCE14;
  --bn-on-danger:  #FF5C56;
  --bn-on-info:    #175cd3;

  /* The faintest wash there is: the drop target on a board, and a hovered band
     on the activity chart. Too weak to survive on a dark surface at this alpha,
     so the dark block raises it rather than each call site guessing. */
  --bn-wash: color-mix(in srgb, var(--bs-primary) 3%, transparent);
  --bn-wash-strong: color-mix(in srgb, var(--bs-primary) 7%, transparent);

  /* -------------------------------------------------------------------------
     Identity hues.

     Six of them, kept away from success/warning/danger because those carry
     meaning elsewhere. They tint an avatar (assigned deterministically from a
     name by fmt.tint(), so one person is one colour on every page) and they
     colour the activity-type mark on the timeline.

     THIS IS NOT A CHART PALETTE and is not held to one. Six hues inside a
     single lightness band cannot clear the categorical CVD floor — the light
     set does not either, and never has — which is legal here and would not be
     on a chart, because an avatar always carries the person's initials and a
     timeline mark always carries the type's own Lucide glyph — a handset, an
     envelope, a ticked calendar. Colour is the second encoding in both, which
     is what let the timeline drop the WORD "Call" beside its mark and still be
     readable to someone who cannot separate the three hues. The palette that IS
     held to the floor, and was measured against it, is `--bn-viz-*` below.

     The dark steps mirror the light set's own lightness spread rather than
     landing on one flat value, so the six stay as separable from each other in
     dark mode as they are in light. Every dark pairing measures at least
     5.9:1 ink-on-tint, against 4.5:1 at the light set's tightest. */
  --bn-hue-indigo:  #4338ca;
  --bn-hue-emerald: #52aa17;
  --bn-hue-amber:   #b45309;
  --bn-hue-fuchsia: #a21caf;
  --bn-hue-sky:     #0369a1;
  --bn-hue-rose:    #be123c;

  --bn-tint-indigo:  #eef2ff;
  --bn-tint-emerald: #ecfdf5;
  --bn-tint-amber:   #fef3c7;
  --bn-tint-fuchsia: #fae8ff;
  --bn-tint-sky:     #e0f2fe;
  --bn-tint-rose:    #ffe4e6;

  /* -------------------------------------------------------------------------
     Data visualisation.

     THESE VALUES WERE VALIDATED, NOT CHOSEN. They are checked against #fff —
     the card surface charts actually sit on, not a generic light surface — for
     lightness band, chroma floor, contrast, and separation under simulated
     protanopia and deuteranopia. Changing a hex here without re-running that
     check is how a palette silently stops working for a red-green colourblind
     reader, which is 8% of men and is not something review catches by looking.

     They replaced a ramp that FAILED it: eight hues that were the status
     colours wearing different names — chart-3 was exactly $success, chart-4
     $warning, chart-5 $danger — despite a comment claiming the opposite. Its
     first two slots measured ΔE 6.2 under deuteranopia (target 8) and 10.4
     under normal vision (floor 15), so indigo and blue were a near-collision
     for everyone. It was never referenced by a stylesheet or a template, so
     nothing rendered wrong; it was a trap set for the first chart to use it.

     --- The ordinal ramp: one hue, light to dark, order in the colour ---
     For bands that HAVE an order — deal-size tiers — where a reader should see
     the sequence in the step, not just in the position. Adjacent steps hold
     ΔL >= 0.06 so they stay distinguishable, and the light end clears 3:1 on
     white, well over the 2:1 an ordinal ramp is held to. Six steps is the
     whole ramp: a seventh could not keep the gap without going black. */
  --bn-viz-seq-1: #7b83ff;
  --bn-viz-seq-2: #676bf6;
  --bn-viz-seq-3: #5453da;
  --bn-viz-seq-4: #423cbd;
  --bn-viz-seq-5: #3224a1;
  --bn-viz-seq-6: #230286;

  /* The single-series hue, and the brand's own primary. Nominal categories —
     company names — take THIS for every bar rather than a step each: the bar
     length already encodes the value, and colouring by value would spend the
     identity channel restating it. */
  --bn-viz-accent: #6366f1;

  /* The diverging pair, for the two charts with a real zero in the middle:
     won above the line and lost below it, signed above estimate and below it.
     Warm against cool, so the poles read as opposite — the midpoint is the
     neutral track below, never a third hue. Measured ΔE 28.8 under protanopia
     and 38.6 under normal vision, comfortably clear of both floors. */
  --bn-viz-pos: #4f46e5;
  --bn-viz-neg: #f04438;

  /* Absence, not a series. The funnel draws the deals it cannot price in this
     grey beside the accent — the emphasis pattern, where one hue carries the
     data and grey carries what is missing. It fails the chroma floor on
     purpose: the floor exists to stop a hue that has gone grey from being
     asked to carry identity, and this one is meant to read as grey. It still
     clears 3:1 on white, and it is 20.8 from the accent, so the distinction
     itself is safe. */
  --bn-viz-muted: #94949e;

  /* Chart chrome. Hairline and solid — never dashed, which reads as a
     projection or a threshold when it is only a grid. */
  --bn-viz-grid: #e9e9eb;
  --bn-viz-axis: #d4d4d8;
  --bn-viz-track: #f4f4f5;
}

/* ---------------------------------------------------------------------------
 * Scrollbars
 *
 * ONE RULE FOR THE WHOLE APP, and it is deliberately the quietest thing here.
 * `scrollbar-width` and `scrollbar-color` are INHERITED properties, so
 * declaring them on :root reaches every scroll container in the project — the
 * page, the board, a board column, the nav, a table card, the search panel, a
 * combobox panel — without a universal selector and without one call site
 * having to opt in. The ::-webkit-* block underneath is the fallback for
 * engines that do not have the standard properties yet; where both are
 * understood the standard pair wins and the pseudo-elements are ignored, which
 * is why the two describe the same thing rather than different things.
 *
 * THE TRACK IS NOT PAINTED. A track is a permanent grey channel down the side
 * of whatever it belongs to — it is drawn whether or not you are scrolling and
 * regardless of how little there is to scroll, so on a board with seven columns
 * it is seven vertical rules that mean nothing. The thumb alone says "there is
 * more below", which is the only thing a scrollbar is for here.
 *
 * `scrollbar-gutter: stable` ON THE PAGE, and that is the reason this block is
 * not purely cosmetic. The document is what scrolls (the sidebar is sticky
 * inside it), so every navigation between a long page and a short one used to
 * add or remove the scrollbar and move the whole layout sideways by its width —
 * most visible switching a board between `?view=table` and back, where the
 * content jumps as the view swaps. Reserving the gutter permanently costs a few
 * pixels that were being fought over anyway and makes the shift impossible. It
 * fixes the same shift at the other end for free: `.bn-search-open` sets
 * `overflow: hidden` on <html> to lock the page behind the search dialog, and an
 * element with `overflow: hidden` is still a scroll container, so the gutter is
 * held there too and summoning search no longer nudges the page.
 * ------------------------------------------------------------------------ */

:root {
  scrollbar-width: thin;
  scrollbar-color: var(--bn-scroll-thumb) transparent;
}

html {
  scrollbar-gutter: stable;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: transparent;
}

/* The transparent border plus `background-clip: padding-box` is what makes the
   thumb read as a floating pill rather than as a bar filling the channel: the
   border is part of the thumb's box, so it reserves the inset without painting
   it. */
::-webkit-scrollbar-thumb {
  background-color: var(--bn-scroll-thumb);
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background-color: var(--bn-scroll-thumb-hover);
}

/* --- Fading a scrolling edge ---------------------------------------------
 *
 * A scroll container marked `data-bn-fade="y"` or `"x"` fades its content out
 * at whichever end it can still be scrolled toward, so a column of cards that
 * continues below the fold ends in a dissolve instead of in a card sliced
 * cleanly in half. A thin thumb says "there is more" only to somebody already
 * looking at the edge of the box; the fade says it in the middle of the content
 * being read.
 *
 * The fade is a MASK, not an overlaid gradient, which is what makes it work on
 * a board column: the column, the card and the page canvas are three different
 * tones, and an opaque gradient can only ever match one of them. A mask takes
 * the pixels out instead, so whatever is behind shows through and dark mode
 * needs no second copy.
 *
 * BOTH ENDS ARE SEPARATE, and each is zero until there is something to scroll
 * toward — `.bn-fade-start` and `.bn-fade-end` are written by
 * partials/scroll-fade.ejs from the container's own scroll position. Fading an
 * edge that is already at the end of its content would dim real content for no
 * reason, and fading a container that does not scroll at all would dim it
 * permanently.
 *
 * The gradient stops are custom properties rather than four separate
 * mask-image declarations so that a state change is one value, and at 0 the two
 * transparent stops collapse onto the black ones and the mask is a no-op. */
[data-bn-fade] {
  --bn-fade-a: 0px;
  --bn-fade-b: 0px;
}

[data-bn-fade].bn-fade-start { --bn-fade-a: var(--bn-fade); }
[data-bn-fade].bn-fade-end { --bn-fade-b: var(--bn-fade); }

[data-bn-fade="y"] {
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--bn-fade-a),
    #000 calc(100% - var(--bn-fade-b)), transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--bn-fade-a),
    #000 calc(100% - var(--bn-fade-b)), transparent 100%);
}

[data-bn-fade="x"] {
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, #000 var(--bn-fade-a),
    #000 calc(100% - var(--bn-fade-b)), transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0, #000 var(--bn-fade-a),
    #000 calc(100% - var(--bn-fade-b)), transparent 100%);
}

/* The board's wrapper carries BOTH the `scaleY(-1)` flip and this mask, and
   they do not interfere: the flip is vertical and the mask is horizontal, so
   `to right` still means right. A vertical fade on that element would have to
   be written upside down; there is no reason to want one, since the wrapper
   does not scroll on that axis. */

/* `line-height-base` is also rejected by Wappler. */
body {
  font-family: "Anthropic Sans Text", var(--bs-font-sans-serif);
  line-height: 1.55;
  background-color: var(--bn-canvas);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Headings are Anthropic Sans Display 600; the tracking is not something the
   Theme tab exposes and 600-weight Anthropic Sans Display at these sizes is
   noticeably loose without it. theme.json's own `headings-font-family` is
   left unset (see the Fonts block above) so `font-family` here is the only
   place the heading font is decided. */
h1, h2, h3, h4, h5, h6 {
  font-family: "Anthropic Sans Display", "Anthropic Sans Text", var(--bs-font-sans-serif);
  font-weight: 500;
}

h1, h2, h3, .bn-page-head h1 {
  letter-spacing: -0.01em;
}

/* ---------------------------------------------------------------------------
 * Icons
 *
 * Every icon in the app is a Lucide icon rendered by the local extension fork
 * (see views/partials/icons.ejs and CLAUDE.md → Icons). These classes are the
 * only place a size is decided, and each one applies to BOTH states of the
 * element: the <i data-lucide="…"> placeholder before the library runs, and the
 * <svg> that replaces it. That is what keeps an icon appearing from shifting
 * anything — the box is already the right size while it is still empty.
 *
 * Sizes are not set as width/height ATTRIBUTES on purpose. Lucide writes its
 * own 24×24 pair onto every svg it makes, and a CSS declaration outranks a
 * presentation attribute, so the class always wins and there is exactly one
 * thing to read.
 *
 * `stroke-width` is a CSS property on SVG, so it overrides Lucide's stroke-width
 * attribute the same way. 2 is Lucide's default and is heavy below about 18px;
 * 1.75 is what makes a 14px icon sit next to 13px text without shouting.
 * ------------------------------------------------------------------------ */

.bn-i,
.bn-i-xs,
.bn-i-sm,
.bn-i-md,
.bn-i-lg,
.bn-i-xl {
  display: inline-block;
  flex: 0 0 auto;
  vertical-align: -0.15em;
  stroke-width: 1.75;
  overflow: visible;
}

.bn-i-xs { width: 0.8125rem; height: 0.8125rem; }
.bn-i-sm { width: 0.875rem;  height: 0.875rem;  }
.bn-i    { width: 1rem;      height: 1rem;      }
.bn-i-md { width: 1.125rem;  height: 1.125rem;  }
.bn-i-lg { width: 1.25rem;   height: 1.25rem;   }
.bn-i-xl { width: 1.5rem;    height: 1.5rem;    stroke-width: 1.6; }

/* An icon in a tinted rounded square, as used by panel headings, empty states
   and modal titles. This is the single most reference-like element in the
   refresh: it is what turns a row of text labels into a scannable list.

   The five tones are a complete set on purpose, even though `success` has no
   call site yet — the same reasoning as the chart ramp at the top of this file.
   A semantic ramp with a hole in it is a trap: the next person needing a green
   one writes an inline style instead of finding out the class was simply never
   added. */
.bn-i-badge {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  flex: 0 0 auto;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

.bn-i-badge-lg {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--bn-radius);
}

.bn-i-badge-primary { background-color: var(--bn-fill-primary); color: var(--bn-on-primary); }
.bn-i-badge-success { background-color: var(--bn-fill-success); color: var(--bn-on-success); }
.bn-i-badge-warning { background-color: var(--bn-fill-warning); color: var(--bn-on-warning); }
.bn-i-badge-danger  { background-color: var(--bn-fill-danger);  color: var(--bn-on-danger);  }
.bn-i-badge-info    { background-color: var(--bn-fill-info);    color: var(--bn-on-info);    }

/* ---------------------------------------------------------------------------
 * Component geometry the Theme tab does not expose
 * ------------------------------------------------------------------------ */

/* Buttons carry icons almost everywhere now, so the flex row and the gap belong
   on .btn itself rather than being re-declared per call site. inline-flex on a
   button also fixes the half-pixel baseline drift an inline svg used to cause. */
.btn {
  --bs-btn-padding-x: 0.9375rem;
  --bs-btn-padding-y: 0.5rem;
  --bs-btn-font-weight: 500;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4375rem;
  white-space: nowrap;
}

.btn-sm {
  --bs-btn-padding-x: 0.75rem;
  --bs-btn-padding-y: 0.3125rem;
  gap: 0.375rem;
}

.btn-lg {
  --bs-btn-padding-x: 1.25rem;
  --bs-btn-padding-y: 0.6875rem;
  --bs-btn-font-size: 0.9375rem;
}

/* A link-button in a row of them ("Edit", "Retire") should not inherit the
   flex gap as extra width when it has no icon. */
.btn-link { gap: 0.3125rem; }


.card {
  --bs-card-spacer-x: var(--bn-pad);
  --bs-card-spacer-y: var(--bn-pad);
  --bs-card-cap-padding-y: 1.125rem;
  --bs-card-cap-bg: transparent;
  --bs-card-border-radius: var(--bn-radius-card);
  --bs-card-inner-border-radius: calc(var(--bn-radius-card) - 1px);
}

/* Every text input, textarea and <select> in the app takes the same border,
   radius and hover/focus language as the filter bar's trigger (.bn-fb-trigger)
   — a modal's dropdown and a list page's filter read as one control family
   rather than two. Only `background-color` is set, never the `background`
   shorthand: .form-select puts its chevron in `background-image`, and the
   shorthand wipes it (see .bn-task-status below for the bug this already
   shipped once). */
.form-control,
.form-select {
  padding: 0.5rem 0.75rem;
  background-color: var(--bs-tertiary-bg);
  border-radius: var(--bn-radius-sm);
  border: none;
  transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
}

.form-control::placeholder { color: var(--bs-tertiary-color); }

/* Deliberately plain selectors, with no `:not()` chain, so a scoped override
   with the same specificity — .bn-task-status:hover, further down this file —
   still wins by source order. A `:not(:disabled)` here would outrank it and
   fight the quieter background that select is tuned to keep. */
.form-control:hover,
.form-select:hover {
  border-color: var(--bn-border-hover);
  background-color: var(--bs-secondary-bg);
}

/* Bootstrap's own focus ring is a hex baked into the compiled CSS by the Sass
   build, so it stops following the Theme tab the moment `theme.json` changes
   the primary colour. `--bs-focus-ring-color` is the same token the filter
   bar's search field focuses with (.bn-fb-search:focus-within) — one ring
   colour, kept in step by the theme rather than restated here. */
.form-control:focus,
.form-select:focus {
  background-color: var(--bn-surface);
  border-color: var(--bn-border-hover);
  box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

.form-label {
  margin-bottom: 0.375rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--bs-emphasis-color);
}

/* Checkboxes, radios and the one switch pick up the same hover/focus
   language as everything above, at their own (much smaller) scale. */
.form-check-input {
  transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
}

.form-check-input:hover:not(:disabled):not(:checked) {
  border-color: var(--bn-border-hover);
}

.form-check-input:focus {
  border-color: var(--bn-border-hover);
  box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

/* Bootstrap's modal is the shell for every create and edit form in the app, and
   at default padding they are the densest surfaces here. */
.modal-content {
  --bs-modal-border-radius: var(--bn-radius-card);
  --bs-modal-border-color: var(--bs-border-color);
  --bs-modal-padding: var(--bn-pad);
  --bs-modal-header-padding: 1.25rem var(--bn-pad);
  box-shadow: var(--bn-lift-lg);
}

.modal-header {
  gap: 0.875rem;
  align-items: center;
}

.modal-title {
  font-size: 1.0625rem;
  letter-spacing: -0.015em;
}

/* The icon tile sits BESIDE the title, never inside it: every form partial
   retitles its modal with `titleEl.textContent = 'Edit …'`, which would delete a
   nested icon on the first edit and leave it missing until a reload. */
.modal-header > .bn-i-badge { margin-right: -0.25rem; }

.modal-footer {
  padding: 1.125rem var(--bn-pad);
  gap: 0.5rem;
}

.modal-footer > * { margin: 0; }

.alert {
  --bs-alert-border-radius: var(--bn-radius);
  --bs-alert-padding-y: 0.75rem;
  --bs-alert-padding-x: 1rem;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 0.8125rem;
}

/* ---------------------------------------------------------------------------
 * App shell
 * ------------------------------------------------------------------------ */

.bn-app {
  display: grid;
  grid-template-columns: var(--bn-sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}

.bn-shell {
  display: flex;
  align-items: stretch;
  flex-direction: column;
  min-width: 0;
}

.bn-content {
  flex: 1 1 auto;
  padding: var(--bn-gutter);
  padding-bottom: calc(var(--bn-gutter) * 2);
  min-width: 0;
  width: 100%;
  max-width: 1440px;
  margin: 0 auto;
}

/* Page header — title block plus right-aligned actions.
 *
 * The reference design's page head is a large title, a one-line description
 * under it in secondary text, and nothing else — and the description is what
 * buys the space: a title alone at 1.5rem needs a bottom margin to breathe,
 * whereas a title with a sentence under it is already a block. Both get more
 * room below than before, because this is the join between the page's identity
 * and its content and it was the tightest gap in the app. */
.bn-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: var(--bn-stack);
}

.bn-page-head h1 {
  color: var(--bs-emphasis-color);
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 1.75rem;
  min-height: 3rem;
  margin-bottom: 0rem;
}

.bn-page-head .bn-page-sub {
  color: var(--bs-secondary-color);
  margin-bottom: 0;
  max-width: 68ch;
}

/* The icon beside a page title. Deliberately not a .bn-i-badge — a title is
   already the loudest thing on the page and a filled tile next to it competes.
   This is a hairline square, the same shape as the badge and a tone quieter. */
.bn-page-icon {
  display: inline-grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  flex: 0 0 auto;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
  background-color: var(--bn-surface);
  color: var(--bs-secondary-color);
  box-shadow: var(--bn-lift);
}

/* A page head whose title carries an icon needs the icon aligned to the title
   line rather than to the centre of title-plus-description. */
.bn-page-head-ident {
  color: var(--bs-emphasis-color);
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  min-width: 0;
}

.bn-page-head-ident .bn-page-icon { margin-top: 0.125rem; }

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

/* THE SIDEBAR IS WHITE AND THE CONTENT AREA IS GREY, which is the other way
   round from how this started. The grey belonged to the chrome and the white to
   the page, so every list and board sat on a white field with white cards on it
   and nothing had an edge. Flipping it makes the working area a canvas: one
   recessed tone, and every card, panel, table and column on it is a white
   surface that reads as an object. It is also what the reference design does. */
.bn-sidebar {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--bs-border-color);
}

.bn-brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  height: var(--bn-topbar-h);
  padding: 0 1.125rem;
  flex: 0 0 auto;
  color: var(--bs-emphasis-color);
  text-decoration: none;
  min-width: 0;
}

.bn-brand:hover {
  color: var(--bs-emphasis-color);
}

/* The wrapper around the light/dark pair. It exists for the fold — see
   partials/sidebar.ejs — and carries no look of its own. */
.bn-brand-word {
  display: flex;
  align-items: center;
  min-width: 0;
}

.bn-brand-logo {
  height: 22px;
  width: auto;
  flex: 0 0 auto;
}

.bn-brand-logo-dark {
  display: none;
}

[data-bs-theme="dark"] .bn-brand-logo-light {
  display: none;
}

[data-bs-theme="dark"] .bn-brand-logo-dark {
  display: block;
}

.bn-nav {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.375rem 0.75rem 1.25rem;
}

/* Sentence case, not uppercase. Four uppercase tracked-out labels down the left
   edge of every page was the single loudest thing in the old sidebar, and they
   are dividers — they should be the quietest. */
/* THE HEIGHT IS WRITTEN OUT rather than left to the line box, and the line
   height with it, because the rail's fold animates this box shut and `auto` is
   not a value anything can move away from. 1rem on an 11px word is a rounder
   number than the 1.55 it inherits and lands within a pixel of it, so nothing
   about the resting layout changed to buy that. */
.bn-nav-section {
  height: 2.625rem;
  padding: 1.25rem 0.625rem 0.375rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  font-weight: 500;
  color: var(--bs-tertiary-color);
}

.bn-nav-section:first-child { height: 1.875rem; padding-top: 0.5rem; }

/* THE ROW HEIGHT IS WRITTEN DOWN, not inherited from whichever child happens to
   be the tallest one in the current state. Left to itself a link is 37.69px
   with a word in it, 40px with a badge beside the word, and 40px with neither —
   three heights for one row, and the fold walks through all three: it holds the
   word laid out while the padding travels to the rail's, so the row swells to
   46px and the whole column drops, then snaps back up by 6px a row the moment
   the fold lets go. 2.5rem is what the badge already forced on the rows that
   have one, so this makes the other seven agree with them rather than the other
   way round. */
.bn-nav-link {
  display: flex;
  align-items: center;
  height: 2.5rem;
  gap: 0.6875rem;
  padding: 0.5rem 0.625rem;
  margin-bottom: 0.0625rem;
  border-radius: var(--bn-radius-sm);
  color: var(--bs-body-color);
  text-decoration: none;
  font-weight: 500;
  transition: background-color 160ms ease, color 160ms ease;
}

.bn-nav-link:hover {
  background-color: var(--bn-nav-hover);
  color: var(--bs-emphasis-color);
}

/* Active item — a primary-tinted pill with primary ink, one step further along
   the same mix the hover uses. It used to be `--bs-tertiary-bg`, a flat grey,
   which meant the two loudest states in the app's most-looked-at control were
   the only two that did not speak the language every tile, queue row and board
   card already speaks (see the tokens' own note). Grey also gave the fill the
   entire job of saying "you are here": at the contrast a recess is allowed to
   have on a white panel, that is about two per cent of luminance.

   The colour is on the FILL, THE LABEL AND THE ICON together, which is the same
   move a.bn-tile makes on hover — border, tint and arrow all going primary at
   once. Three things moving toward one colour is one gesture. */
/* THE INK IS --bn-on-primary, NOT --bs-primary. That token already exists for
   exactly this job — primary ink on a primary wash — and it is the difference
   between passing and failing: #6366F1 on a 12% tint of itself measures
   3.84:1, under the 4.5 a 14px label owes, where --bn-on-primary measures
   6.8:1. It hands over to Bootstrap's dark `*-text-emphasis` in dark mode, so
   the ink lightens with the surface rather than staying an indigo that is
   3.7:1 against a near-black rail. */
.bn-nav-link.active {
  background-color: var(--bn-nav-active);
  color: var(--bn-on-primary);
}

.bn-nav-icon {
  width: 1.125rem;
  height: 1.125rem;
  flex: 0 0 auto;
  stroke-width: 1.75;
  color: var(--bs-tertiary-color);
}

.bn-nav-link:hover .bn-nav-icon {
  color: var(--bn-on-primary);
}

.bn-nav-link.active .bn-nav-icon {
  color: var(--bn-on-primary);
  stroke-width: 2;
}

.bn-nav-count {
  margin-left: auto;
  min-width: 1.375rem;
  padding: 0 0.375rem;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bn-surface);
  color: var(--bs-secondary-color);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
  line-height: 1.5rem;
  /* Declared here rather than under the fold's own selectors so that it applies
     in BOTH directions: the badge fades out when `data-bn-nav-anim` arrives and
     back in when it leaves, and a transition that only existed while the
     attribute was set could not do the second half. It is the one thing in the
     rail that changes shape rather than size — a pill on one side, a dot on the
     other — so it crosses while nobody is looking at it. */
  transition: opacity 140ms ease;
}

.bn-nav-link.active .bn-nav-count {
  background-color: var(--bn-surface);
  color: var(--bn-on-primary);
}

/* Routes that are not built yet. Shown so the shape of the app is visible,
   but inert — drop the page from the `soon` list in sidebar.ejs as each
   one lands. */
.bn-nav-link.bn-soon {
  color: var(--bs-tertiary-color);
  pointer-events: none;
}

.bn-nav-link.bn-soon .bn-nav-icon {
  color: var(--bs-tertiary-color);
}

.bn-sidebar-foot {
  flex: 0 0 auto;
  padding: 0.75rem;
  border-top: 1px solid var(--bs-border-color);
}

.bn-sidebar-foot .dropup { display: block; }

/* ---------------------------------------------------------------------------
 * The collapsed rail
 *
 * `data-bn-nav="collapsed"` on <html>, written by partials/nav-state.ejs before
 * the sidebar is parsed. ABSENCE of the attribute means expanded, so light —
 * the default — costs no rules and nothing has to be un-set.
 *
 * IT IS ONE TOKEN. --bn-sidebar-w is what .bn-app's grid-template-columns is
 * built from, so overriding it moves the whole shell; everything below is only
 * about what the 72px that remain are allowed to hold. `:root[data-bn-nav]` is
 * one specificity step above the bare `:root` the token is declared on, which
 * is what makes the override land without !important.
 *
 * 72px is not a round number picked for looks: .bn-nav's own padding takes
 * 0.75rem a side, leaving 48px for a link — an 18px icon centred in a 48px
 * square, which is the hit area a control is owed.
 *
 * NOTHING HERE ANIMATES, and that is still true of this block: the token flips
 * and the whole shell is laid out at its new size in one go. What moves is one
 * section further down — "Folding the rail" — which slides the panel and the
 * shell back to where they were and lets them travel, so the app is relaid out
 * ONCE rather than once per frame. The distinction is the whole design: a
 * width transition on the grid column would re-lay the boards' columns,
 * re-evaluate the queue's @container queries and re-measure every chart, whose
 * geometry is percentage divs, on every one of eighteen frames.
 * ------------------------------------------------------------------------ */

:root[data-bn-nav="collapsed"] {
  --bn-sidebar-w: 72px;
}

/* The brand. The wordmark is flat artwork with a fixed aspect ratio, so it
   cannot be squeezed — it is swapped for the square mark, the same
   both-in-the-markup-and-CSS-picks-one rule the theme switch's two glyphs and
   the wordmark's own light/dark pair already follow. */
.bn-brand-mark {
  display: none;
  height: 26px;
  width: auto;
  flex: 0 0 auto;
}

:root[data-bn-nav="collapsed"] .bn-brand {
  justify-content: center;
  padding: 0 0.75rem;
}

:root[data-bn-nav="collapsed"] .bn-brand-word { display: none; }
:root[data-bn-nav="collapsed"] .bn-brand-mark { display: block; }

/* A section label is a divider between groups of words. With no words left it
   is a stray line of grey text, so the groups are separated by space instead —
   the padding the label used to occupy, kept as the gap it was providing. */
:root[data-bn-nav="collapsed"] .bn-nav-section {
  height: 0.75rem;
  padding: 0;
  overflow: hidden;
  text-indent: -999em;
}

/* THE ICON IS CENTRED WITH PADDING, not with `justify-content`. Both put an
   18px glyph in the middle of a 48px link, but only one of them is a length —
   and the fold has to move the link from one to the other over 300ms, which a
   keyword cannot do. (48 - 18) / 2 is the 0.9375rem below; change the icon
   size and this changes with it. */
:root[data-bn-nav="collapsed"] .bn-nav-link {
  position: relative;
  gap: 0;
  padding: 0.6875rem 0.9375rem;
}

:root[data-bn-nav="collapsed"] .bn-nav-label { display: none; }

/* THE COUNT BECOMES A DOT. A two-digit pill does not fit beside a centred icon
   in 48px, and shrinking the number until it does produces something nobody can
   read that is still taking the room. What the badge is actually for is "there
   is work in here" — the figure is the detail, and the detail is one click on
   the toggle away. Primary rather than the grey pill it is at full width,
   because at 6px a neutral dot on a white rail is invisible. */
:root[data-bn-nav="collapsed"] .bn-nav-count {
  position: absolute;
  top: 0.4375rem;
  right: 0.5rem;
  min-width: 0;
  width: 0.375rem;
  height: 0.375rem;
  padding: 0;
  border-radius: 999px;
  background-color: var(--bs-primary);
  text-indent: -999em;
  overflow: hidden;
}

:root[data-bn-nav="collapsed"] .bn-nav-link.active .bn-nav-count {
  background-color: var(--bs-primary);
}

/* The user block. The avatar is the whole control at this width — a name and an
   email address are exactly what there is no room for, and the menu they open
   carries both at its head anyway. */
:root[data-bn-nav="collapsed"] .bn-sidebar-foot { padding: 0.75rem 0.5rem; }

/* Same as the nav link above, and the same arithmetic: a 30px avatar in the
   56px the foot's own padding leaves of a 72px rail. */
:root[data-bn-nav="collapsed"] .bn-user-btn {
  gap: 0;
  padding: 0.5rem 0.8125rem;
}

:root[data-bn-nav="collapsed"] .bn-user-text,
:root[data-bn-nav="collapsed"] .bn-user-caret { display: none; }

/* The menu is sized off the sidebar, which has just become narrower than the
   email address inside it. It keeps its full-width size and overhangs the rail
   instead — a dropup is in a positioned layer and has nothing to overflow. */
:root[data-bn-nav="collapsed"] .bn-user-menu {
  width: 16rem;
  max-width: none;
}

/* ---------------------------------------------------------------------------
 * Folding the rail
 *
 * THE SHELL IS LAID OUT ONCE AND THEN SLID BACK INTO PLACE. That sentence is
 * the whole design, and it is what makes an animation affordable on a gesture
 * the block above spent a paragraph refusing to animate.
 *
 * The naive version transitions the grid column, so `grid-template-columns`
 * holds a different length on every frame — and a grid column is layout, so
 * every board re-lays its columns, every @container query on the queue
 * re-evaluates and every chart, whose geometry is percentage divs, re-measures,
 * eighteen times over. What happens instead is a FLIP: --bn-sidebar-w flips to
 * its new value and the whole app is laid out at the new size in ONE go, and
 * then partials/nav-state.ejs measures how far the content moved and puts it
 * back with a `transform`, which is paint rather than layout. Releasing that
 * transform is the animation. Nothing inside the content area is laid out
 * again until it is over — and then it is not laid out again either, because
 * it has been at its final size the whole time.
 *
 * THE PANEL ITSELF DOES ANIMATE ITS WIDTH, and that is the deliberate half of
 * the trade. A stretched grid item computes `width: auto`, which is not a value
 * anything can travel from, so the script pins an explicit width and moves it —
 * relaying out a sticky column holding a brand, fourteen links and a button,
 * which is the one subtree in the app with no board, no container query and no
 * chart in it.
 *
 * THE TWO HALVES ARE THE SAME LENGTH AND THE SAME CURVE, which is what keeps
 * the panel's right-hand edge and the page's left-hand edge together for the
 * whole 300ms. They only meet exactly at the width where .bn-content has not
 * hit its 1440px max — wider than that the page is centred in what is left and
 * travels half as far, so the gap between the two opens rather than closes.
 * Checked both ways: at no width does the page ever cross under the panel.
 *
 * EVERYTHING IN THE PANEL KEEPS ITS EXPANDED GEOMETRY UNTIL THE FOLD IS OVER,
 * because a rail whose contents rearrange on frame one is a panel arriving at
 * a room its furniture has already left. The words are faded rather than
 * dropped, the section headings' height is closed rather than switched off,
 * and the two brand marks cross-fade. `display` is still what hides them at
 * rest — see the block above — and these rules only hold that off for the
 * length of the gesture.
 * ------------------------------------------------------------------------ */

/* For the length of the fold the panel is wider than the column it sits in and
   the page has been pushed back out past the right-hand edge, so something has
   to stop that becoming a horizontal scrollbar. `clip` rather than `hidden`:
   clip is not a scroll container, so the sticky topbar still sticks and the
   `scrollbar-gutter` on this same element is untouched. */
:root[data-bn-nav-anim] { overflow-x: clip; }

:root[data-bn-nav-anim] .bn-sidebar {
  transition: width var(--bn-nav-dur) var(--bn-nav-ease);
}

:root[data-bn-nav-anim] .bn-shell {
  transition: transform var(--bn-nav-dur) var(--bn-nav-ease);
}

/* .bn-nav is `overflow-y: auto`, which computes x to `auto` as well — so a
   label that is wider than the panel has become would be a scrollbar rather
   than a clipped word. */
:root[data-bn-nav-anim] .bn-nav { overflow-x: clip; }

:root[data-bn-nav-anim] .bn-nav-link {
  white-space: nowrap;
  transition: background-color 160ms ease, color 160ms ease,
              gap var(--bn-nav-dur) var(--bn-nav-ease),
              padding var(--bn-nav-dur) var(--bn-nav-ease);
}

:root[data-bn-nav-anim] .bn-nav-section {
  text-indent: 0;
  transition: height var(--bn-nav-dur) var(--bn-nav-ease),
              padding var(--bn-nav-dur) var(--bn-nav-ease),
              opacity 160ms ease;
}

:root[data-bn-nav-anim] .bn-user-btn {
  white-space: nowrap;
  transition: padding var(--bn-nav-dur) var(--bn-nav-ease);
}

/* The words are in the DOM and laid out for the whole gesture whichever way it
   is going, so that the state being travelled TO is the only thing deciding
   which end of the fade they are on. */
:root[data-bn-nav-anim] .bn-nav-label { display: block; }
:root[data-bn-nav-anim] .bn-user-text { display: flex; }
:root[data-bn-nav-anim] .bn-user-caret { display: block; }

:root[data-bn-nav-anim] .bn-nav-label,
:root[data-bn-nav-anim] .bn-user-text,
:root[data-bn-nav-anim] .bn-user-caret {
  transition: opacity 160ms ease, transform 200ms var(--bn-nav-ease);
}

:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-nav-label,
:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-nav-section,
:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-user-text,
:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-user-caret {
  opacity: 0;
}

/* The words leave toward the icon they belong to rather than straight down
   into nothing — 8px is enough to read as a direction and short enough that
   nothing appears to fly. */
:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-nav-label {
  transform: translateX(-0.5rem);
}

/* THEY LEAVE TOGETHER AND COME BACK IN ORDER, and the asymmetry is the point.
   Closing, everything goes at once: the panel is taking the room away and a
   staggered exit would leave words standing in a space that has already gone.
   Opening, the room arrives first and the column fills from the top — 90ms of
   head start, then 10ms a row down --bn-i, which partials/sidebar.ejs counts
   across the sections and the links as one list because the eye reads one
   column.

   The section headings restate their whole transition rather than taking a
   blanket `transition-delay`, because theirs also carries the height and the
   padding — and those two are what keep the headings in step with the panel's
   own width. Delaying them would be delaying the fold. */
:root[data-bn-nav-anim="expanding"] .bn-nav-label,
:root[data-bn-nav-anim="expanding"] .bn-user-text,
:root[data-bn-nav-anim="expanding"] .bn-user-caret {
  transition-delay: calc(90ms + var(--bn-i, 0) * 10ms);
}

:root[data-bn-nav-anim="expanding"] .bn-nav-section {
  transition: height var(--bn-nav-dur) var(--bn-nav-ease),
              padding var(--bn-nav-dur) var(--bn-nav-ease),
              opacity 160ms ease calc(90ms + var(--bn-i, 0) * 10ms);
}

/* THE BADGE IS THE ONE THING IN THE RAIL THAT CHANGES SHAPE rather than size —
   a pill at one end of the fold and a 6px dot at the other, with no in-between
   worth drawing — so it is taken off screen for the gesture and fades back in
   at whichever end it arrives at. Its transition is on the resting rule rather
   than here, or there would be nothing left to fade it back in once this
   attribute is gone.

   The pill's own geometry is restated because the dot's is ABSOLUTE and pinned
   to the corner of the link: without this the badge would jump up to the
   corner on frame one and fade out from there, which is a second thing moving
   in a panel whose whole point is that nothing inside it moves. Colour is left
   alone deliberately — nothing here is ever fully opaque. */
:root[data-bn-nav-anim] .bn-nav-count {
  position: static;
  width: auto;
  height: auto;
  min-width: 1.375rem;
  padding: 0 0.375rem;
  border-radius: var(--bn-radius-sm);
  text-indent: 0;
  opacity: 0;
}

/* THE TWO MARKS CROSS-FADE, which means both of them are on screen at once and
   neither may be in flow. Each is pinned at the position it rests at in the
   state it belongs to, so neither moves when the fold hands over: 1.125rem is
   .bn-brand's own padding, and 1.392rem is where `justify-content: center` puts
   the 26.46px-wide mark in the 47px a 72px rail leaves — 47 and not 48, because
   the sidebar's right border is inside its box. */
:root[data-bn-nav-anim] .bn-brand {
  position: relative;
  overflow: hidden;
}

:root[data-bn-nav-anim] .bn-brand-word,
:root[data-bn-nav-anim] .bn-brand-mark {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  transition: opacity 200ms ease;
}

:root[data-bn-nav-anim] .bn-brand-word { display: flex; left: 1.125rem; }
:root[data-bn-nav-anim] .bn-brand-mark { display: block; left: 1.392rem; opacity: 0; }

:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-brand-word { opacity: 0; }
:root[data-bn-nav-anim][data-bn-nav="collapsed"] .bn-brand-mark { opacity: 1; }

/* The one arriving waits for the one leaving. Two marks at half opacity on top
   of each other for 200ms is a smudge rather than a swap, and which of them is
   arriving is the one thing the direction on this attribute is for. */
:root[data-bn-nav-anim="collapsing"] .bn-brand-mark { transition-delay: 110ms; }
:root[data-bn-nav-anim="expanding"] .bn-brand-word { transition-delay: 110ms; }

/* ---------------------------------------------------------------------------
 * Topbar
 * ------------------------------------------------------------------------ */

.bn-topbar {
  position: sticky;
  top: 0;
  z-index: 1020;
  display: flex;
  align-items: center;
  gap: 1rem;
  height: var(--bn-topbar-h);
  flex: 0 0 auto;
  padding: 0 var(--bn-gutter);
  background-color: var(--bn-canvas);
  max-width: 1440px;
  margin: 0 auto;
  width: 100%;
}

.bn-search {
  position: relative;
  flex: 0 1 440px;
}

/* Both the icon element and the svg Lucide replaces it with — the filter bars on
   the list pages render it as a bare <i data-lucide> with no size class, and an
   svg with no width/height falls back to the 300×150 replaced-element default,
   which is a spectacular way to break a filter bar. */
.bn-search > i[data-lucide],
.bn-search > svg {
  position: absolute;
  left: 0.8125rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1rem;
  height: 1rem;
  stroke-width: 1.75;
  color: var(--bs-tertiary-color);
  pointer-events: none;
}

.bn-search input {
  width: 100%;
  padding: 0.5rem 0.75rem 0.5rem 2.375rem;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bs-secondary-bg);
  color: var(--bs-body-color);
}

.bn-search input::placeholder {
  color: var(--bs-tertiary-color);
}

.bn-search input:disabled {
  cursor: not-allowed;
  color: var(--bs-tertiary-color);
}

.bn-search input:focus {
  outline: 0;
  background-color: var(--bn-surface);
  border-color: var(--bs-border-color);
  box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

.bn-topbar-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* A BARE ICON BUTTON — the topbar's whole vocabulary now, and the one shape
   three different controls share: the sidebar collapse, global search and the
   colour mode.
 *
 * No fill and no outline at rest, so an empty bar reads as empty; a pointer
 * gets --bn-nav-hover and primary ink, which is the same answer a nav link and
 * a user button give, and one step quieter than a .btn would be. That
 * consistency is the point of it being a class rather than three variants: the
 * bar holds one filled button, `New`, and everything else in it is an icon
 * that lights up when pointed at.
 *
 * 2.25rem square, so the 1rem glyph inside has a real hit area — the same
 * reasoning as the reorder chevrons in Settings. The gap between them is
 * tighter than the old 0.75rem because these read as one cluster of chrome
 * rather than as three separate controls. */
.bn-icon-btn {
  display: inline-grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  flex: 0 0 auto;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background-color: transparent;
  color: var(--bs-secondary-color);
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.bn-icon-btn:hover {
  background-color: var(--bn-nav-hover);
  color: var(--bn-on-primary);
}

.bn-icon-btn:focus-visible {
  outline: 0;
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

/* Each icon is wrapped in a span rather than being hidden directly, for the
   reason written up at the theme switch: Lucide REPLACES the <i> with an <svg>
   it builds itself, so a rule written against the placeholder stops matching
   the moment the library runs. The span is not touched by any of that. */
.bn-icon-btn > span { display: grid; }

/* Which glyph the collapse button shows. Scoped through .bn-icon-btn rather
   than written as a bare attribute selector, because `.bn-icon-btn > span`
   above outranks a lone `[data-bn-nav-icon]` and would leave both icons on
   screen — which is exactly what it did. Same shape and same reason as the
   theme switch's pair further down. */
.bn-icon-btn [data-bn-nav-icon="collapsed"] { display: none; }
:root[data-bn-nav="collapsed"] .bn-icon-btn [data-bn-nav-icon="expanded"] { display: none; }
:root[data-bn-nav="collapsed"] .bn-icon-btn [data-bn-nav-icon="collapsed"] { display: grid; }

/* ---------------------------------------------------------------------------
 * Avatars
 * ------------------------------------------------------------------------ */

.bn-avatar {
  width: 32px;
  height: 32px;
  border-radius: 5px;
  background-color: var(--bs-primary);
  /* One of the two literal #fff values in this file, and it is deliberate: this
     is ink ON a brand fill, not a surface. `--bs-primary` is a theme colour and
     does not move with the colour mode, so neither should the initials on top
     of it. The other is the date picker's selected day, for the same reason. */
  color: #fff;
  display: inline-grid;
  place-items: center;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1;
  flex: 0 0 auto;
  overflow: hidden;
  user-select: none;
  position: relative;
}

.bn-avatar img {
  position: absolute;
  inset: auto;
  border-radius: 3px;
  width: 90%;
  height: 90%;
  object-fit: cover;
}

/* A company's favicon, laid OVER its initials rather than instead of them — the
   image is the only child that is positioned, so `onerror="this.remove()"`
   uncovers the initials with no second code path and no layout shift. Which is
   also why this must stay absolutely positioned: as a flow child it would push
   the initials out of the grid cell instead of covering them.

   `contain`, not `cover`: a favicon is a whole mark, and cropping it to fill
   the box cuts the corners off a square logo.

   THE MARK IS THE AVATAR — there is no tile under it. It used to sit on a white
   circle with a hairline ring and 8% padding, which meant a logo drawn to the
   edge of its own canvas met a round mask and lost its corners: clipped on
   every company whose favicon is a filled square, which is most of them. So an
   avatar that HAS an icon drops the fill, the ring, the padding and the radius
   and simply shows the mark. An avatar without one is untouched — the tinted
   circle with initials is still the right answer when there is nothing to show.

   The switch is `:has()` rather than a class the server writes, and that is the
   whole reason it stays honest: `onerror="this.remove()"` takes the image out
   of the DOM, the selector stops matching in the same breath, and the circle
   comes back with the initials. A class on the wrapper would have to be removed
   by the same handler — a second code path for the failure case, which is
   exactly what the absolutely-positioned image was designed to avoid. */
.bn-avatar-favicon {
  object-fit: contain;
}

.bn-avatar:has(.bn-avatar-favicon) {
  background-color: var(--bs-tertiary-bg);
  border-radius: 4px;
  /* The initials are still underneath, and a favicon that is not square lets
     them show through the letterbox. Making them transparent hides them
     without removing them, so the `onerror` path still has something to
     uncover. */
  color: transparent;
}

.bn-avatar-sm {
  width: 24px;
  height: 24px;
  font-size: 0.625rem;
}

/* For a dense list of them — a filter menu's forty companies, where 24px rows
   turn a panel into a scroll. `bn-avatar-sm` is still the list-page size. */
.bn-avatar-xs {
  width: 18px;
  height: 18px;
  font-size: 0.5rem;
}

.bn-avatar-lg {
  width: 44px;
  height: 44px;
  font-size: 0.9375rem;
}

.bn-avatar-muted {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

/* Overlapping stack with a +N counter, used on kanban cards. */
.bn-avatar-stack {
  display: inline-flex;
  align-items: center;
}

.bn-avatar-stack > * {
  box-shadow: 0 0 0 2px var(--bn-surface);
}

.bn-avatar-stack > * + * {
  margin-left: -0.5rem;
}

/* ---------------------------------------------------------------------------
 * User menu
 * ------------------------------------------------------------------------ */

/* The user block at the foot of the sidebar. It is a full-width row rather than
   the pill it was in the topbar: down here it has a panel to fill, and a pill
   floating in a 264px-wide footer looks like it came loose. */
/* Same as .bn-nav-link's height and for the same reason: a name over an email
   address is 2.5px taller than the avatar beside it, so this button is one
   height with the words and another without, and the fold has to walk between
   them. 3.25rem is the first step of the 0.25rem rhythm that clears the
   two-line block. */
.bn-user-btn {
  display: flex;
  align-items: center;
  height: 3.25rem;
  gap: 0.625rem;
  width: 100%;
  padding: 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-body-color);
  text-align: left;
}

.bn-user-btn:hover,
.bn-user-btn[aria-expanded="true"] {
  background-color: var(--bn-nav-hover);
}

.bn-user-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}

.bn-user-name {
  font-weight: 500;
  line-height: 1.3;
  color: var(--bs-emphasis-color);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bn-user-role {
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bn-user-caret { color: var(--bs-tertiary-color); }

/* Between .bn-avatar-sm and the default, because it stands beside two lines of
   text rather than one. Not a fourth avatar size — nothing else needs it. */
.bn-user-btn .bn-avatar {
  width: 1.875rem;
  height: 1.875rem;
  font-size: 0.6875rem;
}

.bn-user-menu {
  width: calc(var(--bn-sidebar-w) - 1.5rem);
  margin-bottom: 0.375rem;
}

.bn-user-menu-head {
  padding: 0.5rem 0.75rem 0.625rem;
  margin-bottom: 0.25rem;
  border-bottom: 1px solid var(--bs-border-color);
}

.bn-user-menu-name {
  font-weight: 500;
  color: var(--bs-emphasis-color);
}

.bn-user-menu-mail {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dropdown-menu {
  --bs-dropdown-border-color: var(--bs-border-color);
  --bs-dropdown-border-radius: var(--bn-radius);
  --bs-dropdown-padding-y: 0.375rem;
  --bs-dropdown-padding-x: 0.375rem;
  --bs-dropdown-item-padding-x: 0.625rem;
  --bs-dropdown-item-padding-y: 0.4375rem;
  --bs-dropdown-link-active-bg: var(--bs-tertiary-bg);
  --bs-dropdown-link-active-color: var(--bs-emphasis-color);
  box-shadow: var(--bn-lift-lg);
}

/* Every item in every menu in the app carries an icon now, so the row is a flex
   line here rather than at each of the call sites. */
.dropdown-menu .dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border-radius: var(--bn-radius-sm);
}

.dropdown-menu .dropdown-item .bn-i-sm,
.dropdown-menu .dropdown-item .bn-i {
  color: var(--bs-tertiary-color);
}

.dropdown-menu .dropdown-item:hover .bn-i-sm,
.dropdown-menu .dropdown-item:hover .bn-i {
  color: var(--bs-secondary-color);
}

.dropdown-menu .dropdown-item.text-danger .bn-i-sm { color: inherit; }

.dropdown-divider { margin: 0.375rem 0.25rem; }

/* ---------------------------------------------------------------------------
 * Segmented control — board / table toggle
 * ------------------------------------------------------------------------ */

.bn-segmented {
  display: inline-flex;
  gap: 0.125rem;
  padding: 0.25rem;
  border-radius: var(--bn-radius);
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
}

.bn-segmented button,
.bn-segmented a {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.3125rem 0.75rem;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-secondary-color);
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}

.bn-segmented button:hover,
.bn-segmented a:hover {
  color: var(--bs-emphasis-color);
}

.bn-segmented .active {
  background-color: var(--bn-surface);
  border-color: var(--bs-border-color);
  color: var(--bs-emphasis-color);
}

.bn-segmented .bn-i-sm,
.bn-segmented .bn-i-xs { color: var(--bs-tertiary-color); }
.bn-segmented .active .bn-i-sm,
.bn-segmented .active .bn-i-xs { color: var(--bs-primary); }

/* ---------------------------------------------------------------------------
 * Kanban
 * ------------------------------------------------------------------------ */

/* THE BOARD'S HORIZONTAL SCROLLBAR IS ABOVE THE CARDS, NOT UNDER THEM.
 *
 * There is no property that says which edge a scrollbar is drawn on — it is
 * always the end edge of the scroll container's own axes. So the scroller is
 * flipped on the Y axis and its single child is flipped back, which leaves the
 * layout, the text and the pointer coordinates exactly where they were and
 * moves only the one thing that is painted in the container's own coordinate
 * space: the scrollbar. Two declarations, no JavaScript, and nothing to keep in
 * sync with a proxy element.
 *
 * It is worth the trick because of where the board sits. Seven columns run to
 * the bottom of the viewport and past it, so a scrollbar on the board's bottom
 * edge is off screen exactly when it has most to say — you cannot see that
 * there are more stages to the right without first scrolling the page down past
 * every card. Above the columns it is next to their heads, which is where the
 * question "how many stages are there" is actually asked.
 *
 * `.bn-board` keeps its name and its flex row, so every selector, every script
 * and every assertion that reaches for it is untouched; the wrapper is new and
 * owns only the scrolling.
 *
 * WHAT THE FLIP DOES NOT BREAK, checked rather than assumed: a transform
 * changes paint, not layout, so the columns' own heights, their max-height and
 * their internal scrollbars are unaffected — a column's scrollbar belongs to
 * `.bn-board-cards`, which is inside the second flip and therefore upright. HTML
 * drag-and-drop hit-testing goes through the same transform as the pointer, so
 * a card still drops where it looks like it drops. Nothing on the board is
 * `position: sticky`, which is the one thing a transformed ancestor would
 * genuinely break. */
.bn-board-scroll {
  overflow-x: auto;
  transform: scaleY(-1);
  /* Reads as the gap ABOVE the columns once flipped: scrollbar, then this, then
     the column heads. */
  padding-bottom: 0.625rem;
}

.bn-board {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  transform: scaleY(-1);
}

/* Wider columns with a real header. 300px was enough for a title and a value and
   nothing else, which is why every card's footer wrapped onto three lines.
   The height budget is `--bn-board-col-max` — see the token for why it is
   deliberately taller than the space visible under the board. */
.bn-board-col {
  flex: 0 0 320px;
  display: flex;
  flex-direction: column;
  max-height: var(--bn-board-col-max);
  border-radius: var(--bn-radius);
  background-color: var(--bn-surface);
}

/* The column head reads as a header rather than as the first row of the list:
   its own background, its own bottom rule, and the stage colour, name, count and
   total on one line. The reference's columns are labelled exactly this way. */
.bn-board-col-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 0.875rem 0 0.875rem;
  flex: 0 0 auto;
  border-radius: calc(var(--bn-radius) - 1px) calc(var(--bn-radius) - 1px) 0 0;
}

.bn-board-col-title {
  font-weight: 600;
  color: var(--bs-emphasis-color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-board-col-total {
  margin-left: auto;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}

/* Small colour chip carrying the stage colour from deal_stages.color. */
/* INLINE-BLOCK, not inline. A width and a height do nothing to an inline
   non-replaced element, so on the boards this only ever worked because its
   parent is a flex container and a flex item is blockified — and in a table cell
   or a <dd>, where there is no flex parent, the chip silently collapsed to
   nothing. Every list page's Stage column was missing its colour. */
.bn-stage-dot {
  display: inline-block;
  vertical-align: 0.05em;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background-color: var(--bs-secondary-color);
  flex: 0 0 auto;
}

.bn-board-cards {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.625rem;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  min-height: 4.5rem;
}

/* An empty column says so. A 4.5rem blank was indistinguishable from a column
   still loading, and on a filtered board that is the difference between "no
   deals here" and "the filter matched nothing".

   IT IS RENDERED ON EVERY COLUMN AND HIDDEN BY `:has()`, rather than rendered
   only on the columns that start empty. Dragging is what makes a column empty
   or full, and the drag handler moves one node between two parents — it does
   not re-render either column, so a server-decided empty state was correct
   until the first drop and then wrong until a refresh: drag the last card out
   of a stage and it stayed blank with nothing saying so, drop a card into an
   empty one and "Nothing at this stage" sat above it.

   The markup is the switch, the same call `fmt.avatar`'s favicon makes. The
   alternative is the drag handler creating and destroying this element, which
   means building markup containing a Lucide icon in JavaScript — the runtime
   icon failure CLAUDE.md warns about — and doing it twice, once per board, to
   keep the two pages' delegated handlers apart. A selector costs neither. */
.bn-board-cards:has(.bn-card) .bn-board-empty {
  display: none;
}

.bn-board-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 1.25rem 0.75rem;
  border: 1px dashed var(--bs-border-color);
  border-radius: var(--bn-radius-sm);
  color: var(--bs-tertiary-color);
  font-size: 0.75rem;
  text-align: center;
  margin: 0;
}

/* A board card sits INSIDE a column that is the same --bn-surface tone, so
   without its own edge a card is visually indistinguishable from the column
   it is in — spacing is the only thing that reads as "a card" at all. The
   border is what the record-page panel version of .bn-card was already
   missing too (compare .bn-board-col, .bn-filterbar and .bn-tile, which all
   pair a surface background with a border rather than relying on the
   background step alone). */
.bn-card {
  display: block;
  padding: 0.875rem;
  border-radius: var(--bn-radius);
  background-color: var(--bn-surface);
  color: inherit;
  text-decoration: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.bn-card:hover {
  color: inherit;
}

.bn-card-title {
  font-weight: 600;
  color: var(--bs-emphasis-color);
  margin-bottom: 0.125rem;
}

.bn-card-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-card-value {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums;
}

/* Drop target while a card is being dragged over a column. */
.bn-board-cards.bn-drop-active {
  outline: 2px dashed var(--bs-primary);
  outline-offset: -4px;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bn-wash);
}

.bn-card.bn-dragging {
  opacity: 0.5;
}

/* ---------------------------------------------------------------------------
 * Tables
 *
 * One block, every table in the app: the five list pages and the eight smaller
 * ones on the record pages. They are the last surface that still read as
 * Bootstrap — a bordered card, a filled grey header stripe, 14px rows on
 * 0.875rem/1.25rem padding, a neutral hover and a primary link in every cell.
 * The rest of the app moved on (the queue rows, the filter bar, the dashboard
 * tiles); this is the same language applied here.
 *
 * `overflow-x: auto`, NOT `overflow: hidden`. The hidden was there to clip the
 * table's square corners to the card's round ones, which a scroll container
 * does just as well — and hidden meant a table wider than its card was CUT OFF
 * rather than scrollable. A data table should scroll; it should never lose a
 * column. Setting overflow-x also makes overflow-y `auto`, which is harmless:
 * these cards are as tall as their content and never scroll vertically.
 *
 * THE CARD HAS NO BORDER. Its fill is --bn-surface against a grey canvas, which
 * is the same thing that makes a queue row read as its own block, and the same
 * reason My day's task list dropped the card it used to sit in. An outline
 * around a shape that is already a different tone from the page is ink spent
 * saying what the tone said.
 * ------------------------------------------------------------------------ */

.bn-table-card {
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius-card);
  overflow-x: auto;
}

.bn-table-card > .table {
  /* The cells paint their own background (the hover rule below is a plain
     background-color, not .table-hover's inset box-shadow), so the table's own
     fill has to be out of the way or it would sit over the card's. */
  --bs-table-bg: transparent;
  --bs-table-color: var(--bs-body-color);
  margin-bottom: 0;
  font-size: 0.8125rem;
}

/* Sentence case, and no fill. An uppercase tracked-out header row was already
   gone; what is gone now is the grey stripe underneath it, which drew a band
   across the top of every list page to say something the weight, the tone and
   the rule under it were already saying three times over. */
.bn-table-card > .table > thead th {
  background-color: transparent;
  border-bottom: 1px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
  font-size: 0.75rem;
  font-weight: 500;
  white-space: nowrap;
}

/* THE HEADER ROW CARRIES NO ICONS, on purpose. The icons in a table belong on
   the DATA, because that is what the eye scans down. Seven of them across the
   header instead would label columns that their own words already label, and
   put the densest row on the page at the top of every list.

   The caret a sortable header carries is not an exception: it is the control's
   own state, not a label for the column. */

/* COMPACT. 0.875rem/1.25rem on a two-line row made a company list of fifteen
   rows a full screen and a half; this is the queue's density, which is the
   thing these pages are read beside. The first and last cells keep a wider
   gutter so the content is not flush against the card's rounded corner. */
.bn-table-card > .table > :not(caption) > * > * {
  padding: 0.5rem 0.75rem;
}

.bn-table-card > .table > :not(caption) > * > *:first-child { padding-left: 1rem; }
.bn-table-card > .table > :not(caption) > * > *:last-child { padding-right: 1rem; }

/* A row rule at full strength on a 0.5rem row is a grid; at half it is a guide.
   The last one goes entirely — the card's own edge is the end of the table. */
.bn-table-card > .table > tbody > tr > * {
  border-bottom-color: color-mix(in srgb, var(--bs-border-color) 55%, transparent);
}

.bn-table-card > .table > tbody > tr:last-child > * {
  border-bottom-width: 0;
}

/* ROWS ARE ALMOST ALWAYS LINKS, and the hover is the same answer the dashboard
   tiles give: --bn-surface-hover, primary mixed into the surface itself, rather
   than the flat --bs-secondary-bg it was. Three surfaces in the app used to
   answer a pointer with a neutral grey — the nav, the tiles and this — and this
   is the last of them. Transitioned, which is cheap between two flat opaque
   colours and would not be against a transparent wash. */
.bn-table-card > .table > tbody > tr > * {
  transition: background-color 140ms ease;
}

.bn-table-card > .table > tbody > tr:hover > * {
  background-color: var(--bn-surface-hover);
}

/* A LINK IN A CELL IS NOT A LINK-COLOURED THING. A domain, an email address, a
   phone number and a company name were all rendering in --bs-primary, so a
   companies list was a column of indigo down the middle of the page and the one
   thing that genuinely wanted the eye — the row title — had no way to say so.
   Body ink, and the underline on hover is what says it is a link. `:not(.btn)`
   keeps the retainer row's own controls out of it. */
.bn-table-card a:not(.btn) {
  color: inherit;
  text-decoration: none;
}

/* `.bn-row-link` is excluded because it is a whole two-line cell — an avatar,
   a title and a sub-line — and text-decoration set on it would run under every
   word in all three. It has its own rule, further down, that underlines the
   title alone. (The avatar itself is safe either way: it is `inline-grid`, an
   atomic inline-level box, which a decoration does not draw across.) */
.bn-table-card a:not(.btn):not(.bn-row-link):hover {
  color: var(--bs-emphasis-color);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* --- A sortable header --------------------------------------------------- */

/* PRESSING THE HEADER IS THE SORT, and it is an <a> for the same reason every
   option in the filter bar is one: choosing an order IS the navigation, so the
   table cannot reach an order the URL disagrees with, there is no client-side
   state to hold, and the whole thing survives a dmxRouting view swap with
   nothing to rebind. See views/partials/table-head.ejs.

   The negative margin lets the hit area have padding without the label moving
   off the column's own left edge. */
.bn-table-card .bn-th-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin: -0.1875rem -0.375rem;
  padding: 0.1875rem 0.375rem;
  border-radius: var(--bn-radius-sm);
  color: inherit;
  text-decoration: none;
  transition: color 140ms ease, background-color 140ms ease;
}

.bn-table-card .bn-th-sort:hover {
  background-color: var(--bn-canvas);
  color: var(--bs-emphasis-color);
  text-decoration: none;
}

/* The caret is invisible until the column is either pointed at or in use, so a
   six-column header is six words rather than six words and six arrows. On hover
   it shows the direction the press would GIVE, which is the only useful thing
   it can say before it is the sorted column. */
.bn-th-caret {
  opacity: 0;
  transition: opacity 140ms ease;
}

.bn-table-card .bn-th-sort:hover .bn-th-caret { opacity: 0.55; }

.bn-table-card > .table > thead th[aria-sort] {
  color: var(--bs-emphasis-color);
  font-weight: 600;
}

.bn-table-card > .table > thead th[aria-sort] .bn-th-caret { opacity: 1; }

.bn-num {
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ---------------------------------------------------------------------------
 * Status pills and empty states
 * ------------------------------------------------------------------------ */

.bn-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0.125rem 0.4375rem;
  border-radius: var(--bn-radius-pill);
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.35;
  white-space: nowrap;
}

/* A pill's icon is the status mark — a dot, a check, a warning triangle — and it
   inherits the pill's tone rather than being tinted separately. */
.bn-pill .bn-i-xs { stroke-width: 2; }

.bn-pill-success {
  background-color: var(--bn-fill-success);
  color: var(--bn-on-success);
}

.bn-pill-warning {
  background-color: var(--bn-fill-warning);
  color: var(--bn-on-warning);
}

.bn-pill-danger {
  background-color: var(--bn-fill-danger);
  color: var(--bn-on-danger);
}

/* A pill with its label dropped — My day's priority column, where the row has
   no room for the word. Equal padding turns the oval into a small square, so
   a bare glyph reads as a mark rather than a badge that lost its text. */
.bn-pill-icon {
  padding: 0.25rem;
  line-height: 1;
}

/* An empty state is now an icon, a heading and a sentence, centred in a column
   narrow enough to read. It used to be two lines of centred text in a 3rem-tall
   band, which read as a rendering failure as often as it read as "nothing here"
   — and there are a lot of them: every list, every tab, every settings panel. */
.bn-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 3.5rem 1.5rem;
  text-align: center;
  color: var(--bs-secondary-color);
}

.bn-empty > .bn-i-badge,
.bn-empty > .bn-i-badge-lg {
  margin-bottom: 1rem;
}

.bn-empty h2 {
  font-size: 1rem;
  color: var(--bs-emphasis-color);
  margin-bottom: 0.375rem;
}

.bn-empty p {
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
  font-size: 0.8125rem;
}


/* ---------------------------------------------------------------------------
 * Auth layout
 * ------------------------------------------------------------------------ */

.bn-auth {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background-color: var(--bn-canvas);
}

.bn-auth-card {
  width: 100%;
  max-width: 400px;
  padding: 2.5rem;
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-card);
  box-shadow: var(--bn-lift-lg);
}

.bn-auth-card h1 {
  font-size: 1.375rem;
  margin-bottom: 0.375rem;
}

/* The brand mark above the title, so the sign-in card is recognisably the same
   product as the shell behind it. */
.bn-auth-mark {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: 1.25rem;
  border-radius: var(--bn-radius);
  background-color: var(--bn-mark-bg);
  color: var(--bn-mark-fg);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ---------------------------------------------------------------------------
 * Records — Phase 2
 *
 * Everything below belongs to the companies and contacts pages and the four
 * partials they share (record-header, activity-timeline, task-list, file-list).
 * Deals and projects reuse all of it in Phases 3 and 4, so it is written
 * against the partials rather than against either page.
 * ------------------------------------------------------------------------ */

/* Avatar tints. Six hues assigned deterministically from a name by
   fmt.tint(), so the same person is the same colour on every page. Kept away
   from success/warning/danger, which carry meaning elsewhere. */
.bn-tint-0 { background-color: var(--bn-tint-indigo);  color: var(--bn-hue-indigo);  }
.bn-tint-1 { background-color: var(--bn-tint-emerald); color: var(--bn-hue-emerald); }
.bn-tint-2 { background-color: var(--bn-tint-amber);   color: var(--bn-hue-amber);   }
.bn-tint-3 { background-color: var(--bn-tint-fuchsia); color: var(--bn-hue-fuchsia); }
.bn-tint-4 { background-color: var(--bn-tint-sky);     color: var(--bn-hue-sky);     }
.bn-tint-5 { background-color: var(--bn-tint-rose);    color: var(--bn-hue-rose);    }

/* Kept as an alias for the icon size classes above — .bn-inline-icon predates
   them and is still the name a dozen call sites use for "an icon on this line
   of text". New markup should reach for .bn-i-sm directly. */
.bn-inline-icon {
  display: inline-block;
  width: 0.875rem;
  height: 0.875rem;
  vertical-align: -0.15em;
  stroke-width: 1.75;
  flex: 0 0 auto;
}

/* --- Filter bar ----------------------------------------------------------
 *
 * Emitted by views/partials/filter-bar.ejs — one component on six list pages.
 * The markup notes explain what it replaced and why; this is what the shapes
 * are for.
 *
 * The bar is a surface rather than a row of controls floating on the canvas:
 * on a list page it is the second thing you look at, and with no edges at all
 * a select and the table's first column used to line up by accident.
 *
 * EVERY CONTROL IN IT IS 2.125rem TALL. Trigger, segmented group, search
 * button and reset link share one height, so the row has a single baseline no
 * matter which mix of controls a page asks for — and the bar keeps its own
 * height when search expands, which is the one animation here that would
 * otherwise move the whole table under it.
 * ------------------------------------------------------------------------ */

.bn-filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.625rem 0.75rem;
  margin-bottom: var(--bn-stack-sm);
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
}

/* One height, one radius, one hover for everything that sits in the bar. */
.bn-filterbar .bn-fb-trigger,
.bn-filterbar .bn-fb-seg,
.bn-filterbar .bn-fb-search,
.bn-filterbar .bn-fb-reset {
  height: 2.125rem;
  border-radius: var(--bn-radius-sm);
}

/* --- Search: a button that becomes a field -------------------------------
 *
 * Search is the rarest control on these pages and had the widest seat. It is
 * an icon button that grows into a field, and it renders ALREADY OPEN when
 * there is a query in the URL — the one case where it is carrying information
 * and hiding it would leave a short list with no visible reason.
 *
 * The transition is on `width`, which is normally the wrong thing to animate;
 * it is right here because the element is a fixed-width flex item either way,
 * so nothing reflows except this one box and its neighbours slide. `max-width`
 * would need a magic number and would ease wrongly at both ends. */
.bn-fb-search {
  position: relative;
  display: flex;
  align-items: center;
  width: 2.125rem;
  background-color: transparent;
  border: 1px solid transparent;
  transition: width 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
              background-color 160ms ease,
              border-color 160ms ease;
}

.bn-fb-search.is-open {
  width: min(22rem, 100%);
  flex: 0 1 22rem;
  background-color: var(--bs-secondary-bg);
  border-color: var(--bs-border-color);
}

.bn-fb-search:focus-within {
  border-color: var(--bs-border-color);
  box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

.bn-fb-search-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background-color: transparent;
  color: var(--bs-secondary-color);
  transition: background-color 160ms ease, color 160ms ease;
}

.bn-fb-search-btn:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-emphasis-color);
}

/* Once the field is open the magnifier stops being a button to the eye and
   becomes the field's own affordance — same glyph, no seat of its own. */
.bn-fb-search.is-open .bn-fb-search-btn {
  background-color: transparent;
  color: var(--bs-tertiary-color);
}

.bn-fb-search-input {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  padding: 0 0.25rem;
  border: 0;
  background: none;
  color: var(--bs-body-color);
  opacity: 0;
  transition: opacity 140ms ease;
}

.bn-fb-search.is-open .bn-fb-search-input {
  opacity: 1;
  transition: opacity 180ms ease 80ms;
}

.bn-fb-search-input:focus { outline: 0; }
.bn-fb-search-input::placeholder { color: var(--bs-tertiary-color); }

/* Chrome's own clear affordance is a grey glyph that ignores the theme and,
   more to the point, clears the FIELD rather than the filter — the list would
   still be narrowed. The link beside it clears both. */
.bn-fb-search-input::-webkit-search-cancel-button { display: none; }

.bn-fb-search-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  margin-right: 0.25rem;
  border-radius: 50%;
  color: var(--bs-tertiary-color);
  text-decoration: none;
}

.bn-fb-search-clear:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-emphasis-color);
}

/* --- Menu ----------------------------------------------------------------
 *
 * A <details> and a panel of links, which is what makes a filter apply on
 * click with no script: the option IS the navigation. It also means the panel
 * survives a dmxRouting view swap with nothing to rebind, the same reason
 * fmt.fields' empty-field disclosure is a <details>.
 *
 * The open animation is a keyframe, not a transition. A <details> goes from
 * `display: none` to laid out in one frame, and there is no interpolable
 * "from" for a transition to start at. */
.bn-fb-menu {
  position: relative;
}

.bn-fb-menu > summary {
  list-style: none;
  cursor: pointer;
}
.bn-fb-menu > summary::-webkit-details-marker { display: none; }
.bn-fb-menu > summary::marker { content: ""; }

.bn-fb-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  max-width: 15rem;
  padding: 0 0.625rem;
  border: 1px solid var(--bs-border-color);
  background-color: var(--bn-surface);
  color: var(--bs-body-color);
  font-size: 0.875rem;
  white-space: nowrap;
  transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}

.bn-fb-trigger:hover {
  border-color: var(--bn-border-hover);
  background-color: var(--bs-secondary-bg);
  color: var(--bs-emphasis-color);
}

.bn-fb-trigger > .bn-i-sm { color: var(--bs-tertiary-color); }

/* The quiet half of "Sort  Last activity". Without it the trigger reads as a
   value with no subject, which is fine for "Any status" and useless for a
   sort key that happens to be called Name. */
.bn-fb-key {
  color: var(--bs-tertiary-color);
}

.bn-fb-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bn-fb-caret {
  margin-left: auto;
  color: var(--bs-tertiary-color);
  transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.bn-fb-menu[open] > summary > .bn-fb-caret { transform: rotate(180deg); }

.bn-fb-menu[open] > .bn-fb-trigger,
.bn-fb-menu[open] > summary.bn-fb-trigger {
  border-color: var(--bn-border-hover);
  background-color: var(--bs-secondary-bg);
  color: var(--bs-emphasis-color);
}

/* A filter that is DOING something says so from across the page — the whole
   reason the bar can drop the Apply button and the row of six identical grey
   selects it used to be. */
.bn-fb-menu.is-on > .bn-fb-trigger {
  border-color: color-mix(in srgb, var(--bs-primary) 45%, var(--bs-border-color));
  background-color: color-mix(in srgb, var(--bs-primary) 8%, var(--bn-surface));
  color: var(--bs-emphasis-color);
  font-weight: 500;
}

.bn-fb-menu.is-on > .bn-fb-trigger > .bn-i-sm { color: var(--bs-primary); }

/* Above the sticky topbar (1020) and below Bootstrap's modal backdrop (1040),
   because the filter bar sits close enough to the top of a long list that a
   few lines of scroll put an open panel behind the chrome. */
.bn-fb-panel {
  position: absolute;
  z-index: 1030;
  top: calc(100% + 0.375rem);
  left: 0;
  min-width: 100%;
  max-width: 20rem;
  width: max-content;
  padding: 0.3125rem;
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
  box-shadow: var(--bn-lift-lg);
  animation: bn-fb-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* A panel opening near the right edge of the content area would otherwise be
   clipped by the viewport; the last two controls in a bar are the sort menu
   and the reset, and the sort menu is the one that reaches furthest. */
.bn-fb-menu:last-of-type .bn-fb-panel { left: auto; right: 0; }

@keyframes bn-fb-in {
  from { opacity: 0; transform: translateY(-0.375rem) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.bn-fb-opts {
  display: flex;
  flex-direction: column;
  max-height: 17rem;
  overflow-y: auto;
}

/* Reused verbatim by the form combobox below (.bn-sel-menu) for its panel and
   options — same markup classes, not a duplicate ruleset. Its option is an
   <a> here and a <button> there, so the panel works with no fetch and the
   form keeps its value with no navigation; the four UA resets below
   (background/border/width/font/cursor) are only meaningful to the button
   case and are no-ops on an anchor. */
.bn-fb-opt {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 0.5rem;
  padding: 0.4375rem 0.5rem;
  border: 0;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-body-color);
  font: inherit;
  font-size: 0.875rem;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.bn-fb-opt:hover,
.bn-fb-opt:focus-visible {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-emphasis-color);
  outline: 0;
}

.bn-fb-opt > .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-fb-opt-label {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bn-fb-opt-count {
  flex: 0 0 auto;
  color: var(--bs-tertiary-color);
  font-variant-numeric: tabular-nums;
}

.bn-fb-opt.is-on {
  color: var(--bs-emphasis-color);
  font-weight: 500;
}

/* The tick holds its seat whether or not it is showing, so the labels in a
   panel do not step sideways as the selection moves down it. */
.bn-fb-tick {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--bs-primary);
  visibility: hidden;
}

.bn-fb-opt.is-on .bn-fb-tick { visibility: visible; }

/* A colour dot carries a stage, a tag or a status tone into the panel, which
   is what a <select> could never do — an <option> cannot hold markup. */
.bn-fb-dot {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background-color: var(--bn-fb-dot, var(--bs-tertiary-color));
}

.bn-fb-dot-success { background-color: var(--bs-success); }
.bn-fb-dot-warning { background-color: var(--bs-warning); }
.bn-fb-dot-danger  { background-color: var(--bs-danger); }
.bn-fb-dot-info    { background-color: var(--bs-info); }
.bn-fb-dot-primary { background-color: var(--bs-primary); }
.bn-fb-dot-quiet   { background-color: var(--bs-tertiary-color); }

.bn-fb-find {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem 0.4375rem;
  margin-bottom: 0.125rem;
  border-bottom: 1px solid var(--bs-border-color);
}

.bn-fb-find > .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-fb-find input {
  flex: 1 1 auto;
  min-width: 8rem;
  border: 0;
  background: none;
  color: var(--bs-body-color);
  font-size: 0.875rem;
}

.bn-fb-find input:focus { outline: 0; }
.bn-fb-find input::placeholder { color: var(--bs-tertiary-color); }

.bn-fb-none {
  margin: 0;
  padding: 0.5rem;
  color: var(--bs-tertiary-color);
  font-size: 0.875rem;
}

/* --- Segmented filter ----------------------------------------------------
 *
 * For a SHORT, ORDERED set, where showing every choice at once costs less than
 * the menu it saves. The thumb slides between equal columns, which is why the
 * group is a grid rather than a flex row: `--bn-seg-i` moving the thumb by a
 * whole column is only true when the columns are the same width, and equal
 * columns are also what stops the whole bar shifting when a two-word label
 * replaces a one-word one. */
.bn-fb-seg {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--bn-seg-n, 2), minmax(0, 1fr));
  align-items: stretch;
  padding: 0.1875rem;
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
}

.bn-fb-seg::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: 0.1875rem;
  bottom: 0.1875rem;
  left: 0.1875rem;
  width: calc((100% - 0.375rem) / var(--bn-seg-n, 2));
  transform: translateX(calc(var(--bn-seg-i, 0) * 100%));
  border-radius: calc(var(--bn-radius-sm) - 0.125rem);
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  transition: transform 240ms cubic-bezier(0.34, 1.3, 0.44, 1);
}

.bn-fb-seg > a {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3125rem;
  padding: 0 0.6875rem;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  white-space: nowrap;
  text-decoration: none;
  transition: color 160ms ease;
}

.bn-fb-seg > a:hover { color: var(--bs-emphasis-color); }

.bn-fb-seg > a.is-on {
  color: var(--bs-emphasis-color);
  font-weight: 500;
}

.bn-fb-seg > a > .bn-i-xs { color: var(--bs-tertiary-color); }
.bn-fb-seg > a.is-on > .bn-i-xs { color: var(--bs-primary); }

/* --- Reset ----------------------------------------------------------------
 *
 * Pushed to the far end, and only rendered when there is something to clear —
 * a control that does nothing on most page loads is the same decoration the
 * Apply button was. The count is what makes it worth a seat: on a wrapped bar
 * the filters that are on are not all visible at once. */
.bn-fb-reset {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-left: auto;
  padding: 0 0.625rem;
  border: 1px solid transparent;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.bn-fb-reset:hover {
  background-color: var(--bs-secondary-bg);
  border-color: var(--bs-border-color);
  color: var(--bs-emphasis-color);
}

.bn-fb-reset > .bn-i-xs { color: var(--bs-tertiary-color); }
.bn-fb-reset:hover > .bn-i-xs { color: var(--bs-danger); }

.bn-fb-reset-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.125rem;
  height: 1.125rem;
  padding: 0 0.3125rem;
  border-radius: 999px;
  background-color: var(--bn-invert-bg);
  color: var(--bn-invert-fg);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .bn-fb-search,
  .bn-fb-search-input,
  .bn-fb-caret,
  .bn-fb-seg::before { transition: none; }
  .bn-fb-panel { animation: none; }
  /* Named explicitly: the drop-up rule above sets `animation-name`, and it
     outranks a bare `.bn-fb-panel` selector, so the shorthand alone would let
     the flipped panel animate here while the un-flipped one does not. */
  .bn-sel-menu[data-bn-sel-drop="up"] > .bn-fb-panel { animation: none; }
}

/* --- Form combobox and swatch picker, extending the filter bar's menu -----
 *
 * Emitted by views/partials/form-select.ejs — a <select> past ~8 options
 * (a company or contact picker) or a fixed colour palette (the two swatch
 * pickers), styled as one control family with the filter bar rather than as
 * a plain <select> with no styling hook of its own.
 *
 * THE REAL <select> IS THE SOURCE OF TRUTH, NOT A DECORATION. It stays in the
 * DOM, keeps its `id` (tools/browser_pass.py's cdp.fill sets a real <select>'s
 * value and dispatches input/change — see the .bn-sel-native rule below) and
 * its `name` (so the form still submits it with no JS at all). It is visually
 * hidden with the standard clip technique, not `display: none`, and taken out
 * of the tab order (`tabindex="-1"`) — <summary> is a native tab stop on its
 * own, so keyboard focus lands on the visible trigger, not the invisible one.
 *
 * THE PANEL REUSES THE FILTER BAR'S CLASSES DIRECTLY: .bn-fb-panel, .bn-fb-opts,
 * .bn-fb-opt, .bn-fb-find, .bn-fb-none, .bn-fb-tick, and the bn-fb-in keyframe.
 * None of those rules mention the filter bar by name in their selector, so a
 * <details> here gets the identical panel with no new CSS — only the trigger
 * differs, because a form trigger has to match its row's .form-control height
 * and a filter trigger has to match the bar's fixed 2.125rem.
 *
 * THE TRIGGER IS `.form-select` PLUS `.bn-sel-trigger`, not a new box model.
 * Composing onto `.form-select` is what makes it align pixel-for-pixel with a
 * plain <select> beside it in the same row — the border, radius, padding,
 * hover and focus-ring all come from the base layer above for free. The one
 * override is `background-image: none`: `.form-select` carries Bootstrap's
 * own chevron there, which would sit underneath the Lucide one this markup
 * draws itself. Source order in this file is what makes that override win
 * with no !important — see the base layer's own comment on the same trick.
 * ------------------------------------------------------------------------ */

.bn-sel {
  position: relative;
  display: block;
  width: 100%;
}

/* Visually hidden, not display:none — an invisible element cannot be
   scrolled to, focused programmatically in the same way, or (the part that
   matters here) reliably targeted by a headless-Chrome native-setter fill if
   any layout engine special-cases zero-size elements. Off-screen and
   zero-area via clip is the standard technique for exactly this reason. */
.bn-sel-native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.bn-sel-menu { position: relative; }
.bn-sel-menu > summary { list-style: none; cursor: pointer; }
.bn-sel-menu > summary::-webkit-details-marker { display: none; }
.bn-sel-menu > summary::marker { content: ""; }

.bn-sel-trigger {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  width: 100%;
  background-image: none;
}

.bn-sel-trigger > .bn-i-sm { flex: 0 0 auto; color: var(--bs-tertiary-color); }

.bn-sel-value {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.bn-sel-value.bn-sel-placeholder { color: var(--bs-tertiary-color); }

.bn-sel-trigger > .bn-fb-caret { flex: 0 0 auto; margin-left: 0; }

.bn-sel-menu[open] > .bn-sel-trigger {
  border-color: var(--bn-border-hover);
  background-color: var(--bs-secondary-bg);
}

.bn-sel-menu[open] > summary > .bn-fb-caret { transform: rotate(180deg); }

/* A disabled picker (the edit-locked company field) keeps the same chrome
   rather than reverting to a plain grey box — dimming it is what says "this
   is still that control" instead of "this became something else". The
   underlying <select> already carries `disabled`, which is what keeps it out
   of the submitted body; this only has to stop the panel from opening. */
.bn-sel[data-bn-sel-disabled] .bn-sel-trigger {
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
}

.bn-sel-menu[data-bn-sel-align="right"] .bn-fb-panel { left: auto; right: 0; }

/* DROP UP WHEN THERE IS NO ROOM BELOW, and the attribute is written by JS
   because whether there is room is a runtime fact — the same field is halfway
   up a page in one caller and at the foot of a modal in another, so no
   server-passed option of the `align` kind can answer it. `form-select`'s
   captured `toggle` handler measures on open and clears on close; both this
   and `.bn-date-panel` are `.bn-fb-panel`, so one rule covers the combobox and
   the calendar. Preferred direction is still DOWN — the flip only happens when
   below does not fit AND above does, so a picker in a short window does not
   flip into a second clipped position. */
.bn-sel-menu[data-bn-sel-drop="up"] > .bn-fb-panel {
  top: auto;
  bottom: calc(100% + 0.375rem);
  animation-name: bn-fb-in-up;
}

@keyframes bn-fb-in-up {
  from { opacity: 0; transform: translateY(0.375rem) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

/* ESCAPE A CLIPPING ANCESTOR, written by the same measured-on-open handler as
   the drop direction above. `.bn-table-card` scrolls sideways, and an element
   that scrolls on one axis clips on the other — so /retainers' row picker was
   opening inside the card and growing it rather than overlaying the page. Only
   the positioning scheme changes here: the panel keeps its own fill, radius,
   shadow and animation, and every caller with nothing clipping it never gets
   this attribute at all. `left` and `top` are inline, from the trigger's rect;
   the four resets are what stop the absolute rules above and the drop-up rule
   below from fighting them. Above `.bn-fb-panel`'s 1030 so it clears a sticky
   header, below a Bootstrap modal's backdrop, which it has no business over. */
.bn-sel-menu[data-bn-sel-fixed] > .bn-fb-panel,
.bn-sel-menu[data-bn-sel-fixed][data-bn-sel-drop="up"] > .bn-fb-panel,
.bn-sel-menu[data-bn-sel-fixed][data-bn-sel-align="right"] .bn-fb-panel {
  position: fixed;
  right: auto;
  bottom: auto;
  z-index: 1040;
}

/* --- Swatch: the two colour pickers ---------------------------------------
 *
 * A grid rather than the menu's column, and each option is the colour
 * itself rather than a label beside a dot — the one place in the app where
 * the value being chosen IS a colour and the control should look like one. */
.bn-sel-opts-swatch {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.25rem, 1fr));
  gap: 0.3125rem;
  padding: 0.125rem;
}

.bn-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--bn-radius-sm);
  background: none;
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease;
}

.bn-swatch:hover,
.bn-swatch:focus-visible { background-color: var(--bs-secondary-bg); outline: 0; }

.bn-swatch.is-on { border-color: var(--bs-emphasis-color); }

.bn-swatch-dot {
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  background-color: var(--bn-swatch, var(--bs-tertiary-color));
}

/* The trigger's own dot, smaller — it sits beside a text label rather than
   carrying the whole control the way the panel's tiles do. */
.bn-sel-trigger > .bn-swatch-dot {
  width: 0.875rem;
  height: 0.875rem;
  flex: 0 0 auto;
}

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

/* --- Segmented control inside a form ---------------------------------------
 *
 * Emitted by views/partials/form-segmented.ejs. Visually the filter bar's
 * .bn-fb-seg — same sliding thumb, same grid-of-equal-columns reasoning — but
 * a form has to POST a value rather than navigate to one, so the options are
 * <button>s writing into a real hidden <select> instead of <a>s. Reserved for
 * a SHORT, ORDERED set that fits the width of the column it sits in; a
 * five-option enum in a col-4 is a menu instead; see the CLAUDE.md judgement
 * calls for which fields did and did not earn this. */
.bn-seg-field { position: relative; width: 100%; }

.bn-seg {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--bn-seg-n, 2), minmax(0, 1fr));
  align-items: stretch;
  height: 2.375rem;
  padding: 0.1875rem;
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-sm);
}

.bn-seg::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: 0.1875rem;
  bottom: 0.1875rem;
  left: 0.1875rem;
  width: calc((100% - 0.375rem) / var(--bn-seg-n, 2));
  transform: translateX(calc(var(--bn-seg-i, 0) * 100%));
  border-radius: calc(var(--bn-radius-sm) - 0.125rem);
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  box-shadow: var(--bn-lift);
  transition: transform 240ms cubic-bezier(0.34, 1.3, 0.44, 1);
}

.bn-seg-opt {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3125rem;
  padding: 0 0.5rem;
  border: 0;
  background: none;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  white-space: nowrap;
  cursor: pointer;
  transition: color 160ms ease;
}

.bn-seg-opt:hover { color: var(--bs-emphasis-color); }
.bn-seg-opt:focus-visible { outline: 0; }

.bn-seg-opt.is-on {
  color: var(--bs-emphasis-color);
  font-weight: 500;
}

.bn-seg-opt > .bn-i-xs { color: var(--bs-tertiary-color); }
.bn-seg-opt.is-on > .bn-i-xs { color: var(--bs-primary); }

.bn-seg.bn-seg-disabled { opacity: 0.6; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .bn-seg::before { transition: none; }
}

/* --- Record picker ---------------------------------------------------------
 *
 * Emitted by views/partials/form-record.ejs — the topbar's global search,
 * inside a form. It borrows from BOTH of the components it sits between and
 * defines almost nothing of its own, which is the point: the trigger is
 * .form-select + .bn-sel-trigger, exactly as the combobox's is, so it lines up
 * pixel-for-pixel with a plain select in the same row; the panel is
 * .bn-fb-panel, so the border, radius and opening animation are the filter
 * bar's; and the rows inside it are .bn-search-group / .bn-search-hit verbatim,
 * so a result set looks like the one CTRL-K already shows. What is left here is
 * a scroll cap, a clear button and the three rules that make a <button> behave
 * where the topbar had an <a>. */
.bn-rec {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  width: 100%;
}

/* The menu takes the row's free width and the clear button takes its own, so
   the trigger stays the same height as the field beside it whether or not a
   record is picked. */
.bn-rec-menu { flex: 1 1 auto; min-width: 0; }

/* Taller than a combobox's panel because results come in five groups and a
   heading costs a row — measured against a modal, where the panel may not run
   past the dialog's own foot. */
.bn-rec-panel {
  max-height: 22rem;
  overflow-y: auto;
  padding: 0.375rem;
}

/* .bn-search-hit assumes an <a>; these are <button>s, because a pick is not a
   navigation. Three declarations is the whole difference. */
.bn-rec-hit {
  width: 100%;
  border: 0;
  background-color: transparent;
  text-align: left;
  cursor: pointer;
}

.bn-rec-hit:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

/* The kind mark on the trigger. A wrapper rather than the icon itself, because
   its innerHTML is replaced when the kind changes and the element doing the
   replacing must not be the one carrying the layout. */
.bn-rec-mark {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  color: var(--bs-tertiary-color);
}

.bn-rec-clear {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--bn-radius-sm);
  background-color: transparent;
  color: var(--bs-tertiary-color);
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}

.bn-rec-clear:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-emphasis-color);
}

.bn-rec-clear:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

/* The prompt and the two failure sentences, which reuse the filter bar's own
   empty line rather than inventing a third. */
.bn-rec-results .bn-fb-none {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  margin: 0;
  padding: 0.875rem 0.625rem;
}

/* --- Priority slider -------------------------------------------------------
 *
 * Emitted by views/partials/form-priority.ejs. A sibling of .bn-seg above, with
 * one trade made differently: the options show their ICON only and the SELECTED
 * one widens to say its own name, so four choices cost four glyphs plus one
 * label instead of four labels. The reason that is allowed here and not for,
 * say, a billing period is written up in the partial's header — the marks have
 * to carry the order on their own.
 *
 * THE SELECTED STEP IS THE THUMB, AND IT IS FLAT. This used to be a ::before
 * tile sliding under a row of icons with a --bn-lift shadow on it, and the word
 * read out in a fixed 3.5rem gutter to the LEFT of the track. Two problems, one
 * fix: the shadow made a second surface float inside a form control that every
 * other control in this app draws as one flat box, and the gutter made the word
 * look like a readout sitting next to a picker rather than part of one. Now the
 * fill, the ink and the word are all on the button itself — one object, one
 * border, no shadow — and the motion is a step GROWING rather than a tile
 * travelling, which is what the flex-grow transition is.
 *
 * THE TONE RAMP IS THE POINT. grey → blue → amber → red, built from the same
 * --bn-fill-* / --bn-on-* pairs the status pills and task-status chips already
 * use — so High and Urgent are the amber and red they will be on the row
 * afterwards. Only `normal` differs from its pill, which is untinted on a list
 * so the ordinary case does not shout across forty rows; inside a four-mark
 * control the ordinary case has to look like a step rather than a gap. The
 * reasoning is restated at PRIORITY_SCALE in views/tasks.ejs, which is where
 * the mapping actually lives. Nothing new is defined here; dark mode follows
 * for free because those tokens already do. */
.bn-prio-field {
  display: block;
  width: 100%;
}

.bn-prio-track {
  display: flex;
  align-items: stretch;
  gap: 0.125rem;
  width: 100%;
  height: 2.375rem;
  padding: 0.1875rem;
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-sm);
}

/* Every step is an equal share of the track until one is chosen, and the chosen
   one takes 2.2 shares — measured, not picked: the longest word in the scale
   plus its glyph needs ~64px, and 2.2 gives it 90 in the narrowest column this
   control is used in with the other three still wide enough to hit. The spring
   is .bn-seg::before's, so this is the filter bar's motion language rather than
   a second one, even though nothing slides any more. */
.bn-prio-step {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3125rem;
  overflow: hidden;
  border: 0;
  padding: 0;
  background-color: transparent;
  border-radius: calc(var(--bn-radius-sm) - 0.125rem);
  color: var(--bs-tertiary-color);
  cursor: pointer;
  transition: flex-grow 240ms cubic-bezier(0.34, 1.3, 0.44, 1),
              background-color 160ms ease,
              color 160ms ease;
}

.bn-prio-step:hover { color: var(--bs-emphasis-color); }

/* An outline, not a ring made of shadow — see the block header. Inset, so it
   cannot escape the track it sits inside. */
.bn-prio-step:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

.bn-prio-step > .bn-i-xs { flex: 0 0 auto; stroke-width: 2; }

/* The word exists in the markup on every step and is shown on exactly one —
   which is what lets the label change without a line of JavaScript touching
   text, and therefore without ever putting an icon inside something whose
   textContent gets assigned. */
.bn-prio-word {
  display: none;
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
}

.bn-prio-step.is-on {
  flex-grow: 2.2;
  background-color: var(--bs-secondary-bg);
  color: var(--bs-secondary-color);
}

.bn-prio-step.is-on .bn-prio-word { display: inline; }

/* The four rungs. Each sets the selected step's fill and its ink from one pair,
   so a retuned theme colour carries through both. */
.bn-prio-step.is-on[data-bn-prio-tone="quiet"] {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-secondary-color);
}

.bn-prio-step.is-on[data-bn-prio-tone="info"] {
  background-color: var(--bn-fill-info);
  color: var(--bn-on-info);
}

.bn-prio-step.is-on[data-bn-prio-tone="warning"] {
  background-color: var(--bn-fill-warning);
  color: var(--bn-on-warning);
}

.bn-prio-step.is-on[data-bn-prio-tone="danger"] {
  background-color: var(--bn-fill-danger);
  color: var(--bn-on-danger);
}

.bn-prio-track.bn-prio-disabled { opacity: 0.6; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .bn-prio-step { transition: none; }
}

/* A `success` rung. The scale did not need one while its only caller was task
   priority, where nothing is good — it is a ramp from "whenever" to "now". A
   project's health starts at the good end and moves away from it, so the first
   step is green rather than grey. */
.bn-prio-step.is-on[data-bn-prio-tone="success"] {
  background-color: var(--bn-fill-success);
  color: var(--bn-on-success);
}

/* The same control at card size. `.bn-prio-sm` only shrinks — the track, the
   ink, the tones, the widening step and the keyboard behaviour are all the ones
   above, so the health control on a board card is the same object as the
   priority control in a modal rather than a second one that resembles it.
   Sized against the 0.6875rem footer it sits in: at full height it was the
   tallest thing on the card and read as the card's primary content. */
.bn-prio-sm .bn-prio-track {
  height: 1.625rem;
  padding: 0.125rem;
  gap: 0.0625rem;
  border-radius: 999px;
}

.bn-prio-sm .bn-prio-step {
  gap: 0.1875rem;
  border-radius: 999px;
}

.bn-prio-sm .bn-prio-word { font-size: 0.6875rem; }

/* --- Form date picker ------------------------------------------------------
 *
 * Emitted by views/partials/form-date.ejs. The block below used to say date
 * inputs get no custom widget because no CSS can restyle a native picker; that
 * is still true and this does not try. The native input stays as the value and
 * is visually hidden by .bn-sel-native exactly as the combobox's <select> is —
 * what is replaced is the POPUP, which is ours to draw.
 *
 * The panel is .bn-fb-panel verbatim, so the shadow, radius, border and opening
 * animation are the filter bar's with no new rules; only the grid inside is
 * new. Two of these sit side by side in a row of col-6s, which is what the
 * 15.5rem width is measured against — it fits inside .bn-fb-panel's own 20rem
 * cap and stays clear of the modal's edge. */
.bn-date {
  position: relative;
  display: block;
  width: 100%;
}

.bn-date-panel {
  width: 15.5rem;
  padding: 0.5rem;
}

.bn-date-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem;
  margin-bottom: 0.375rem;
}

.bn-date-title {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
}

/* A 1.75rem square rather than a bare glyph — the reorder arrows taught this
   one: a 12px mark in a zero-padding button is not a target. */
.bn-date-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-secondary-color);
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.bn-date-step:hover {
  background-color: var(--bn-canvas);
  color: var(--bs-emphasis-color);
}

.bn-date-dow,
.bn-date-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.125rem;
}

.bn-date-dow {
  margin-bottom: 0.125rem;
}

.bn-date-dow > span {
  text-align: center;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--bs-tertiary-color);
  line-height: 1.6;
}

.bn-date-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-body-color);
  font-size: 0.8125rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.bn-date-cell:hover { background-color: var(--bn-canvas); }
.bn-date-cell:focus-visible { outline: 0; border-color: var(--bs-primary); }

/* The days either side of the month are shown rather than blanked — a calendar
   with holes at both ends is harder to read than one that runs on — but they
   recede to the same tertiary ink .bn-none uses for an absent value. */
.bn-date-cell.is-outside { color: var(--bs-tertiary-color); }

.bn-date-cell:disabled {
  opacity: 0.4;
  cursor: default;
  background: none;
}

/* Today is a ring, the selection is a fill. Both are true on the common case of
   a task due today, so .is-on comes AFTER and restates border-color — equal
   specificity, so source order is what makes the fill win. */
.bn-date-cell.is-today {
  border-color: var(--bs-border-color);
  font-weight: 600;
}

.bn-date-cell.is-on,
.bn-date-cell.is-on:hover {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
  /* Ink on a brand fill — see the note on .bn-avatar, which is the only other
     literal #fff here and is white for the identical reason. */
  color: #fff;
  font-weight: 600;
}

.bn-date-foot {
  display: flex;
  gap: 0.25rem;
  margin-top: 0.375rem;
  padding-top: 0.375rem;
  border-top: 1px solid var(--bs-border-color);
}

.bn-date-act {
  flex: 1 1 0;
  padding: 0.3125rem 0.5rem;
  border: 0;
  border-radius: var(--bn-radius-sm);
  background: none;
  color: var(--bs-primary);
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 120ms ease;
}

.bn-date-act:hover { background-color: var(--bn-canvas); }
.bn-date-act-quiet { color: var(--bs-secondary-color); }

/* --- Native controls left alone, tightened where the theme cannot reach ---
 *
 * number, and every date input OUTSIDE the two that got the picker above, keep
 * the native control — the base layer's border/radius/focus is already on them
 * and a hand-built calendar is only worth it where the field is a focal point
 * of the form. The inline task rows are not that. Two gaps that layer cannot
 * reach: */

/* Chrome draws the calendar/clock glyph in a fixed dark grey, which
   disappears into a dark surface. Inverted only in dark mode, and only the
   glyph — the field's own colours are untouched. */
[data-bs-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1);
}

/* The browser's spinner buttons sit flush against the field's own border in
   most engines already; this only stops them looking pinned to the text the
   moment a number field also carries a width utility, where the default
   padding otherwise crowds the two together. */
input[type="number"] { padding-right: 0.5rem; }

/* The one input-group (the stage form's probability %): the addon takes the
   field's own radius on its outer corner and the same border colour, so the
   two read as one control rather than a field with a label stapled on. */
.input-group-text {
  background-color: var(--bs-tertiary-bg);
  border-color: var(--bs-border-color);
  color: var(--bs-secondary-color);
}

.input-group > .form-control:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.input-group > .input-group-text:last-child { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: 0; }

/* --- Notice strip -------------------------------------------------------- */

.bn-notice {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.875rem 1.125rem;
  margin-bottom: var(--bn-stack-sm);
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-left: 3px solid var(--bs-warning);
  border-radius: var(--bn-radius-sm);
  font-size: 0.8125rem;
}

.bn-notice > span { flex: 1; }
.bn-notice .bn-inline-icon,
.bn-notice .bn-i { color: var(--bs-warning); }

/* --- Table rows that are links ------------------------------------------- */

.bn-row-link {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: inherit;
  text-decoration: none;
}

/* NOT primary. The row already answers the pointer with a tint of its own, and
   a title that also turns indigo makes two announcements for one gesture — on a
   list where every row is a link, so the colour never distinguishes anything.
   The underline is what says "this is the link", which is what it is for. */
.bn-row-link:hover .bn-row-title {
  color: var(--bs-emphasis-color);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.bn-row-title {
  display: block;
  font-weight: 500;
  color: var(--bs-emphasis-color);
}

.bn-row-sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.125rem 0.5rem;
  margin-top: 0.0625rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* Each fragment is an icon plus one value and stays whole; the ROW may still
   wrap between fragments. Without the nowrap a sub-line reading "Monthly from
   2026-07-21" breaks after the hyphen in the date, which looks like a rendering
   fault rather than a wrap. */
.bn-row-sub > * {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  white-space: nowrap;
}

.bn-row-sub .bn-i-xs { color: var(--bs-tertiary-color); }

/* THE SEPARATOR IS A MIDDOT, because most of these fragments no longer carry an
   icon. A globe before a domain and a pin before a city were labelling two
   values whose own shape already says which is which, and the sub-line under a
   deal repeated the company favicon that is the row's avatar three characters
   to its left. With the glyphs gone the fragments need something between them,
   and a dot is the smallest thing that is not a second column of ink. */
.bn-row-sub > * + *::before {
  content: "\00B7";
  margin-right: 0.4375rem;
  color: var(--bs-tertiary-color);
}

/* A cell that is an icon plus a value — a phone number, a website, a location.
   Used across the list tables, where a column of bare strings gives the eye
   nothing to lock onto. */
.bn-cell-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  /* A name, a date or a domain that wraps mid-value is harder to read than a
     table that scrolls — and now that .bn-table-card scrolls rather than
     clipping, the wider table is the better trade. "Kenan Blue North" was
     breaking onto three lines in a squeezed Owner column. */
  white-space: nowrap;
}

.bn-cell-icon .bn-i-sm,
.bn-cell-icon .bn-i-xs { color: var(--bs-tertiary-color); }

/* Tabular figures, so money columns line up on the decimal. */
.bn-num { font-variant-numeric: tabular-nums; }

/* A tag rides ALONGSIDE a row title rather than filling a column of its own —
   ICP on a company, Quiet on a deal. It is deliberately smaller and tighter
   than .bn-pill, which is the whole content of the cell it sits in: this one
   has to share a line with a name without pushing it about. Same reason it
   carries no icon — a mark plus a word beside a title is three things on one
   line where the word alone is one. */
.bn-row-tag {
  display: inline-block;
  margin-left: 0.25rem;
  padding: 0 0.3rem;
  border-radius: 0.25rem;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
  vertical-align: 1px;
}

.bn-row-tag-primary {
  background-color: var(--bn-fill-primary);
  color: var(--bn-on-primary);
}

.bn-row-tag-warning {
  background-color: var(--bn-fill-warning);
  color: var(--bn-on-warning);
}

/* --- Breadcrumb ---------------------------------------------------------- */

/* IT IS A SENTENCE, NOT A ROW OF ICONS. Every crumb used to carry the record
   type's Lucide mark — a building before "Companies", a kanban board before
   "Pipeline" — which is the same glyph the sidebar entry two centimetres to
   the left already shows, in front of a word that already says it. Three
   glyphs across a 12px line is most of why this read as decoration rather than
   as a trail.

   And the trail's own end is now the loudest thing on it: the crumbs behind
   you are secondary ink and the record you are on is emphasis ink at medium
   weight, so "where am I" is answered by the line's shape rather than by
   reading all of it. It is a step up in size too — 13px, the app's small
   reading size, because at 12px in tertiary grey it was the quietest text on a
   page whose first job is to say where you are. */
.bn-breadcrumb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3125rem;
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  margin-bottom: 0.875rem;
}

.bn-breadcrumb a {
  color: inherit;
  text-decoration: none;
}

.bn-breadcrumb a:hover { color: var(--bs-primary); text-decoration: underline; text-underline-offset: 0.15em; }

/* The last crumb — the record itself. */
.bn-breadcrumb-here {
  color: var(--bs-emphasis-color);
  font-weight: 500;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The separator between crumbs. A chevron rather than the `/` this used to
   print, which is what the reference does and what stops a breadcrumb reading
   as a file path. */
.bn-breadcrumb-sep { color: var(--bs-tertiary-color); flex: 0 0 auto; }

/* --- Record header ------------------------------------------------------- */

/* THE HEAD IS AN IDENTITY AND A SET OF ACTIONS, AND NOTHING ELSE NOW.

   It used to carry a `<dl>` of four meta pairs and a row of four tiles as
   well, which between them put eight labelled facts above the tabs — and every
   one of them was also in the Details card one tab click away. The rail on the
   right is where those live now, permanently, on every tab rather than on one:
   see "The record rail" below. What is left here is the mark, the name, the
   badges that qualify the name, and what you can DO to the record.

   `align-items: center`, not flex-start: with the meta list gone the identity
   block is two lines at most, so the actions sit on its axis rather than
   hanging off the top of it. */
.bn-record-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: var(--bn-stack);
}

.bn-record-ident {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  min-width: 0;
}

.bn-record-title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.bn-record-title-row h1 {
  font-size: 1.5rem;
  margin: 0;
}

.bn-record-sub {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.bn-record-sub .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-record-actions {
  display: flex;
  gap: 0.5rem;
  flex: 0 0 auto;
}

/* --- Summary tiles ------------------------------------------------------- */

.bn-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
  gap: 0.7rem;
  margin-bottom: var(--bn-stack);
}

.bn-tile {
  padding: 0.9rem 1.1rem;
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius);
  /* min-height, not height: the money tiles carry a month-on-month readout
     beside the value, and at a narrow column that readout wraps under it (see
     .bn-tile-value below). A fixed height would put the wrapped line outside
     the tile it belongs to. Nothing moves when it does not wrap. */
  min-height: 7.5rem;
}

/* Sentence case, and an icon on the label line. The reference's stat cards are
   labelled exactly this way — a small icon, a quiet label, a big number — and
   the uppercase micro-label this replaced was the single most repeated piece of
   visual noise in the app: four of them across the top of nine pages. */
.bn-tile-label {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--bs-body-color);
}

/* The icon is the label's own mark and sits at the head of the line, whatever
   size it is given. `margin-left: auto` belongs to ONE icon only — the corner
   arrow on a tile that navigates — and lives with the rest of that tile's rules
   under `a.bn-tile`. Declared here it also caught the leading icon on every
   tile that has no arrow, which is all four on the pipeline, delivery and
   retainer summaries and all four in a record header: one icon, alone on the
   line, pushed to the far right of its own label. */
.bn-tile-label .bn-i-xs,
.bn-tile-label .bn-i-sm,
.bn-tile-label .bn-i-md { color: var(--bs-tertiary-color); }

.bn-tile-value {
  margin-top: auto;
  /* A row rather than a block, so the trend readout sits beside the figure and
     shares its baseline. It WRAPS: a tile is one column of an auto-fit grid
     with a 12.5rem floor, so at four across on a narrow window the two ends
     stop fitting on one line long before any viewport breakpoint would fire.
     Reaching for the container's own constraints rather than a media query is
     the same rule the chart bar rows learned. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.5rem;
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums;
}

/* --- The month-on-month readout beside a tile value ---------------------- */

/* Built by fmt.trend. The DIRECTION is the coloured thing and the figure is
   not: three saturated sentences across the top of the page would shout louder
   than the numbers they are annotating, and the colour is already carrying the
   whole message. Weight and size come back down to the label's, so the tile
   still has exactly one headline. */
.bn-trend {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0;
  margin-left: auto;
  color: var(--bs-tertiary-color);
}

.bn-trend .bn-i-sm { flex: 0 0 auto; }

/* The figure is data and reads brighter than the words next to it; "last
   month" stays at the tile label's own tertiary tone and needs no rule of its
   own — it is what .bn-trend already sets. */
.bn-trend-was { color: var(--bs-body-color); font-weight: 500; }

/* The `--bn-on-*` inks rather than the raw theme colours: this is ink sitting
   on a surface, not a dot or a wash, and those tokens are the pair the app has
   already tuned for legibility in BOTH modes — raw `--bs-success` is one fixed
   hue that does not know which way round the surface is. */
.bn-trend-success .bn-i-sm { color: var(--bn-on-success); }
.bn-trend-danger  .bn-i-sm { color: var(--bn-on-danger); }
.bn-trend-warning .bn-i-sm { color: var(--bn-on-warning); }

/* An empty tile keeps its label and its hint — "0 open deals" is the answer —
   but the value itself steps back to the weight of the label rather than
   rendering a dash as a headline. See the note in partials/record-header. */
.bn-tile-value-blank {
  font-size: 1rem;
  font-weight: 400;
  color: var(--bs-tertiary-color);
}

/* A TILE'S DESCRIPTION SITS ABOVE ITS VALUE. The summary tiles on /pipeline,
   /projects and /retainers say what they are counting BEFORE they say the
   number, so the label and its qualifier read as one block and the figure is
   the last thing on the tile — which is the only arrangement that keeps four
   headline numbers on one baseline when only some of them carry a description.

   `.bn-tile-value` already carries `margin-top: auto`, so the free space in the
   tile collects between the description and the value; that only works in a
   flex column, which is why the switch is `:has()` on the markup rather than a
   modifier class — the same reasoning as the favicon avatar.

   It was written as a `:has()` for a case that has since gone: record-header
   drew tiles whose hint sat BELOW the value (`.bn-tile-hint`), and bottom-
   aligning that pair would have lifted the figure of every tile that had one. A
   record's figures are `partials/rail-stats` in the rail now and the header
   draws no tiles at all, so every `.bn-tile` left in the app is a page summary
   and `.bn-tile-hint` has no emitter. The `:has()` stays regardless: "the
   markup decides" is the right shape, and a modifier class would be a second
   thing to keep in step. */
.bn-tile:has(.bn-tile-note) {
  display: flex;
  flex-direction: column;
}

.bn-tile-note {
  margin-top: 0.2rem;
  font-size: 0.65rem;
  line-height: 1.35;
  color: var(--bs-tertiary-color);
}

/* --- Tabs ---------------------------------------------------------------- */

/* Tabs sit on the canvas rather than in a card, so the rule under them is the
   only thing separating eight labels from the panel below. It needs room on both
   sides of it: this is the join the record pages felt tightest at. */
.bn-tabs {
  border-bottom: 1px solid var(--bs-border-color);
  gap: 0.25rem;
  margin-bottom: var(--bn-stack);
  flex-wrap: wrap;
}

.bn-tabs .nav-link {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0.625rem 0.75rem;
  margin-bottom: -1px;
  font-size: 0.8125rem;
  color: var(--bs-body-color);
}

.bn-tabs .nav-link:hover { color: var(--bs-emphasis-color); }
.bn-tabs .nav-link .bn-i-sm { color: var(--bs-tertiary-color); }

/* Selected is the emphasis ink and the underline — not a weight change, same
   rule as the sidebar's own active nav item. */
.bn-tabs .nav-link.active {
  color: var(--bs-emphasis-color);
  background: transparent;
  border-bottom-color: var(--bs-primary);
}

.bn-tabs .nav-link.active .bn-i-sm { color: var(--bs-primary); }

.bn-tab-count {
  display: inline-block;
  min-width: 1.25rem;
  padding: 0 0.375rem;
  border-radius: 999px;
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.bn-tabs .nav-link.active .bn-tab-count {
  background-color: var(--bn-fill-primary);
  color: var(--bn-on-primary);
}

.bn-tab-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 1rem;
}

/* --- The record rail ------------------------------------------------------
 *
 * THE PAGE IS TWO COLUMNS FROM THE TABS DOWN: what you are working in on the
 * left, what the record IS on the right.
 *
 * Every record page used to be a vertical stack — head, four tiles, tabs, then
 * one tab's worth of content — with the record's own facts split across three
 * places that each said some of them. The header meta named four, the tiles
 * named four more, and an Overview or Details tab held the rest behind a click
 * that then hid the timeline you were reading. So the answer to "how big is
 * this deal" depended on which tab you happened to be on, and two of the three
 * places were saying the same thing twice.
 *
 * The rail is that record, once, permanently, beside whatever tab is open. It
 * is not a fourth surface language: the stats read like the summary tiles, the
 * rows are `.bn-detail-list` exactly as the old Details card's were, and a
 * section folds through a `<details>` with the queue's own caret. What changed
 * is where they sit and that none of them is duplicated anywhere else.
 *
 * A FIXED RAIL, NOT A FRACTION. `20rem` rather than a `2fr` share, because the
 * rail holds label-and-value rows whose comfortable width does not depend on
 * the window — it is the LEFT column that should take every pixel a wider
 * screen offers, since that is where a table, a board or a timeline lives.
 * Below 1100px it drops under the main column, which is the same breakpoint
 * `.bn-two-col` uses.
 *
 * It is deliberately NOT sticky. A project's rail runs to five sections and is
 * taller than the viewport, and a sticky column that is itself too tall simply
 * stops scrolling with the page halfway down — worse than not being pinned.
 */

.bn-record-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: var(--bn-stack-lg);
  align-items: start;
}

@media (max-width: 1100px) {
  .bn-record-grid { grid-template-columns: minmax(0, 1fr); }
}

.bn-record-main { min-width: 0; }

.bn-record-rail {
  display: flex;
  flex-direction: column;
  gap: var(--bn-stack);
  min-width: 0;
}

/* One block of the rail. Same surface, radius and padding as `.bn-card`, and
   it is not that class because a `<details>` needs its padding on the parts
   rather than on the box — a summary that stops short of the block's own edge
   has a dead strip either side of a control whose whole job is to be pressed. */
.bn-rail-card {
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius-card);
  padding: var(--bn-pad-sm);
}

/* A FOLDING SECTION, IN THE QUEUE'S OWN LANGUAGE. Same `<details>`, same
   chevron rotating 90 degrees, same reason: no JavaScript, so nothing to
   rebind after a dmxRouting view swap and no guard flag to remember. The head
   is the whole width of the block so the hit area is the line, not the word. */
.bn-rail-sec { padding: 0; }

.bn-rail-head {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  padding: var(--bn-pad-sm);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.bn-rail-head::-webkit-details-marker { display: none; }

.bn-rail-caret {
  flex: 0 0 auto;
  color: var(--bs-tertiary-color);
  transition: transform 0.15s ease;
}

.bn-rail-sec[open] > .bn-rail-head .bn-rail-caret { transform: rotate(90deg); }

.bn-rail-title {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
}

.bn-rail-head:hover .bn-rail-title,
.bn-rail-head:hover .bn-rail-caret { color: var(--bs-primary); }

/* A count beside the section name, on the two sections that hold a list. Same
   shape and reasoning as the queue group's: a number, not a badge. */
.bn-rail-count {
  margin-left: auto;
  font-size: 0.6875rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--bs-tertiary-color);
}

/* The body starts under the head, so its top padding is the head's bottom. */
.bn-rail-body {
  padding: 0 var(--bn-pad-sm) var(--bn-pad-sm);
}

/* THE HEADLINE FIGURES, AS A GRID RATHER THAN A ROW OF CARDS.
   These are the four `.bn-tile`s that used to sit under the record header, at
   the width the rail gives them: two across, no icon on the label, and the
   figure a size down from a page tile's. The label-above-value order and the
   quiet hint underneath are the tiles' own, so the same four numbers read the
   same way they did — they are simply no longer four white rectangles on a
   page that already had a head, a stage bar and a tab bar above the content. */
.bn-rail-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.875rem 0.75rem;
}

.bn-rail-stat { min-width: 0; }

.bn-rail-stat-label {
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-rail-stat-value {
  margin-top: 0.125rem;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* An empty figure steps back to the label's weight rather than rendering a
   dash as a headline — the same call `.bn-tile-value-blank` makes, and made
   the same way: by recognising the dash here rather than asking every caller
   to pass a flag. */
.bn-rail-stat-blank {
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--bs-tertiary-color);
}

.bn-rail-stat-hint {
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--bs-tertiary-color);
}

/* Inside the rail a detail list is the whole point of the column, so it loses
   the page card's roomier row and takes the tables' compact rhythm. */
.bn-rail-body .bn-detail-list { font-size: 0.75rem; }

.bn-rail-body .bn-detail-list > div {
  grid-template-columns: minmax(4.5rem, 7.5rem) minmax(0, 1fr);
  gap: 0.5rem;
  padding: 0.4375rem 0;
}

/* A one-line note inside a rail section — "nothing recurring against this
   project", "no deal behind this". It replaces the `.bn-empty` block those
   sections used to draw: an icon tile, a heading and two sentences of prose is
   a page-sized empty state, and in a 20rem column it was taller than the
   section it was apologising for. */
.bn-rail-note {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--bs-secondary-color);
}

.bn-rail-note a { color: var(--bs-primary); }

/* A rail section's own action — "Create project" on a won deal with none. */
.bn-rail-act { margin-top: 0.75rem; }

/* A CONTROL SITTING WITH THE FIGURES, which today is one thing: a project's
   health. It takes the stat label's own type so the row reads as a fifth
   entry in the grid above rather than as a form dropped underneath it, and it
   is separated by a rule because the thing below the line is the one you can
   change. */
.bn-rail-control {
  margin-top: 0.875rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--bs-border-color);
}

.bn-rail-control .bn-prio-field { margin-top: 0.375rem; }

/* --- Cards and detail lists ---------------------------------------------- */

.bn-two-col {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: var(--bn-stack-sm);
  align-items: start;
}

@media (max-width: 1100px) {
  .bn-two-col { grid-template-columns: minmax(0, 1fr); }
}

.bn-card {
  padding: var(--bn-pad);
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius-card);
}

/* A CARD HEADING IS A HEADING NOW, NOT A MICRO-LABEL. It was 11px uppercase
   tracked-out secondary text, which is the convention for a field label, not for
   the title of a panel — so every card looked like a form and nothing on a
   record page had a hierarchy. It is sentence case at reading size, in the
   emphasis colour, and it carries a Lucide icon. Bottom margin is doubled: the
   heading was closer to the content below it than the content was to itself. */
.bn-card-heading {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
  margin-bottom: 1.125rem;
}

.bn-card-heading .bn-i,
.bn-card-heading .bn-i-sm { color: var(--bs-tertiary-color); }


.bn-detail-list { margin: 0; font-size: 0.8125rem; }

/* BOTH ENDS SHRINK, and the label end has a floor rather than a fixed width.
   A flat 11rem is fine in the three-fifths card and wrong in the two-fifths one
   beside it: the contact panel is about 400px inside its padding, so the label
   claimed 160 of it and an ordinary email address broke across two lines with
   the `.se` on its own. Same failure the chart bar rows had, in a different
   shape — see the note on minmax there. */
.bn-detail-list > div {
  display: grid;
  grid-template-columns: minmax(5.5rem, 10rem) minmax(0, 1fr);
  gap: 0.75rem;
  padding: 0.625rem 0;
  border-top: 1px solid var(--bs-border-color);
}

.bn-detail-list > div:first-child { border-top: 0; padding-top: 0; }
.bn-detail-list > div:last-child { padding-bottom: 0; }

.bn-detail-list dt {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-weight: 400;
  color: var(--bs-secondary-color);
}

.bn-detail-list dt .bn-i-sm { color: var(--bs-tertiary-color); }
.bn-detail-list dd { margin: 0; color: var(--bs-emphasis-color); overflow-wrap: anywhere; }

/* A column holding one mark rather than a phrase — the owner avatar on the
   list pages. It claims no more width than the header word needs. */
.bn-col-tight { width: 1%; white-space: nowrap; }

.bn-nowrap { white-space: nowrap; }

/* THE UNIVERSAL EMPTY MARKER, and the point of it is that it recedes. An em
   dash set in the same ink as a real value makes a column of nothing read as
   loudly as a column of something — which on the company list is most of the
   MRR and Open deals columns, and on a record card was half the rows. Tertiary
   colour, and never bold. */
.bn-none { color: var(--bs-tertiary-color); }

/* --- The empty half of a detail list ------------------------------------- */

/* A <details> holding the fields that have no value, so the card reads as what
   the record HAS. No JavaScript, so nothing to rebind after a view swap. */
.bn-fields-rest { margin-top: 0.875rem; }

.bn-fields-rest > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem;
  margin-left: -0.5rem;
  border-radius: var(--bn-radius-sm);
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  cursor: pointer;
  list-style: none;
}

.bn-fields-rest > summary::-webkit-details-marker { display: none; }
.bn-fields-rest > summary::marker { content: ''; }
.bn-fields-rest > summary:hover { color: var(--bs-emphasis-color); background-color: var(--bn-canvas); }

.bn-fields-rest > summary .bn-i-xs {
  color: var(--bs-tertiary-color);
  transition: transform 0.15s ease;
}

.bn-fields-rest[open] > summary .bn-i-xs { transform: rotate(180deg); }

/* The disclosed rows are quieter than the ones above the fold — they are the
   fields the record is MISSING, so the labels matter and the dashes do not. */
.bn-fields-rest > .bn-detail-list { margin-top: 0.5rem; }
.bn-fields-rest > .bn-detail-list > div:first-child { border-top: 1px solid var(--bs-border-color); padding-top: 0.625rem; }

/* --- Activity composer ---------------------------------------------------
 *
 * Three controls, down from six. Migration 019 took the direction select and
 * the minutes field off the table entirely, and the type row went from five
 * chips to three — so what is left is "what kind", "which day" and "what
 * happened", which is the whole of what an activity is.
 *
 * The type row and the date share one line, because both are one-glance
 * choices and neither is wide; the body gets the full width underneath, since
 * it is the only field anybody actually types into. That is also why the date
 * is up THERE rather than in the foot beside the button: the old layout put it
 * in a row of leftovers under the textarea, where three small controls of
 * different widths read as a settings strip rather than as part of the entry.
 */

.bn-composer {
  padding: 0.875rem;
  margin-bottom: var(--bn-stack);
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-card);
}

.bn-composer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.625rem;
}

.bn-composer-end {
  display: flex;
  align-items: center;
  justify-content: stretch;
  gap: 0.5rem;
}

.bn-composer-types {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

/* .btn-check + label, styled as a quiet segmented control rather than a row of
   outline buttons — the type is a mode, not three separate actions. The
   selected chip goes primary-tinted with --bn-on-primary ink, which is the
   answer the nav, the tiles and the board drop target all give: a selection in
   this app is primary, never a neutral, and this was one of the last places
   still saying it with a grey and a shadow. */
.bn-composer-types .btn {
  --bs-btn-padding-y: 0.3125rem;
  --bs-btn-padding-x: 0.625rem;
  --bs-btn-font-size: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 1px solid transparent;
  border-radius: var(--bn-radius-sm);
  color: var(--bs-secondary-color);
  background-color: transparent;
}

.bn-composer-types .btn:hover { background-color: var(--bn-nav-hover); color: var(--bn-on-primary); }

.bn-composer-types .btn-check:checked + .btn {
  background-color: var(--bn-nav-active);
  color: var(--bn-on-primary);
  border-color: transparent;
}

.bn-composer-types .btn-check:focus-visible + .btn {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

/* partials/form-date's trigger is a .form-select, which stretches. It is a
   date and eleven characters wide whatever the window does. */
.bn-composer-when { flex: 0 0 auto; }
.bn-composer-when .bn-date { width: 11.5rem; }
.bn-composer-when .bn-sel-trigger { --bs-form-select-padding-y: 0.3125rem; font-size: 0.8125rem; }

.bn-composer-body { font-size: 0.875rem; resize: vertical; }

.bn-composer-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.625rem;
}

/* --- Timeline ------------------------------------------------------------
 *
 * WHAT SOMEBODY WROTE IS A BLOCK; WHAT THE DATABASE OBSERVED IS A LINE. That
 * is still the whole of the redesign. A human entry takes the queue row's
 * language — an opaque --bn-surface fill on the page canvas, no border, a small
 * radius, a hover — so each one has an edge you can see without an outline
 * being drawn round it, and a stage change stays a single quiet line on the
 * rail between them.
 *
 * WHAT CHANGED AFTER THAT IS THE HEIGHT. Every entry is one head row and one
 * line of body until it is pressed, so forty of them are forty equal blocks and
 * the columns down each edge — how long ago on the left, the record and the
 * author on the right — can be read straight down. The card IS the disclosure:
 * its <summary> is the whole collapsed face, so pressing anywhere on it opens
 * it, and what opening reveals is the rest of the body and the one control that
 * can rewrite it. No JavaScript, so nothing to rebind after a view swap — the
 * same call the rail sections and the queue's groups make.
 *
 * FOUR THINGS CAME OFF THE ENTRY AND NOTHING WENT ON. The type's word went (the
 * dot's mark and colour were saying it a second time), the author's name went
 * (an avatar says who in two letters and a fixed 18px seat, where a full name
 * was the widest and most repetitive thing on the row), the lead line's
 * emphasis weight went (one note does not have a headline and a caption), and
 * the month headings went with the grouping that carried them — the row already
 * says how long ago it was, and the exact day is beside it in tertiary ink.
 */

/* THE RAIL IS ONE LINE FOR THE WHOLE TIMELINE. It used to be a ::before on each
   month's own <ol>, inset half a rem top and bottom — so it died above the
   first entry of every group, restarted below the last, left the month heading
   standing in a gap, and stopped dead at both ends of the list.

   IT FADES AT EACH END RATHER THAN STOPPING. A spine that ends on a hard pixel
   reads as a rendering fault wherever it lands, and where it should land
   depends on the offset of the first dot, which differs between a card row and
   a system row. A gradient owes nothing to either: it is opaque wherever there
   are entries and gone before the edge. */
.bn-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bn-timeline::before {
  content: "";
  position: absolute;
  left: 13px;
  top: 0;
  bottom: 0;
  width: 1px;
  background-image: linear-gradient(
    to bottom,
    transparent 0,
    var(--bs-border-color) 1.25rem,
    var(--bs-border-color) calc(100% - 1.25rem),
    transparent 100%
  );
}

.bn-timeline-item {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
}

/* THE DOT IS A FILLED TINT, not a hairline circle with a coloured glyph inside
   it. At 27px with a 12px mark, a hue applied to the stroke alone is a few
   pixels of colour and the three types were not tellable apart at arm's length;
   the tint fills the whole mark's seat, so the type is answered by the shape
   before the icon inside it is read at all. Opaque, which is also what lets it
   punch through the rail behind it with no halo to keep in step with whatever
   surface the tab content happens to sit on. */
.bn-timeline-dot {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  margin-top: 0.3125rem;
  width: 27px;
  height: 27px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

/* A THIRD OF THE WHEEL APART, from the --bn-hue/--bn-tint pairs the avatars
   use — so both halves are already measured against both surfaces (see the
   token block: every pairing is 4.5:1 or better in light and 5.9:1 in dark).
   It was sky/indigo/amber, which is two blues and an amber: email and call
   differed by about fifteen degrees of hue on a twelve-pixel glyph. Indigo is
   also the app's accent, so a type wearing it read as a selection. */
.bn-type-call    { background-color: var(--bn-tint-emerald); color: var(--bn-hue-emerald); }
.bn-type-email   { background-color: var(--bn-tint-sky);     color: var(--bn-hue-sky); }
.bn-type-meeting { background-color: var(--bn-tint-amber);   color: var(--bn-hue-amber); }

/* The entry itself — a <details>. Same fill, radius and hover as .bn-tq-row,
   and no border for the same reason: the surface against the canvas IS the
   edge. */
.bn-timeline-card {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.75rem 0.5625rem;
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius-sm);
  transition: background-color 0.12s ease;
}

.bn-timeline-item:hover .bn-timeline-card { background-color: var(--bn-surface-hover); }

/* THE WHOLE COLLAPSED CARD IS THE PRESS TARGET. A caret would be a second
   thing to aim at inside a control that is already entirely aimable, which is
   why there is none. The default marker goes in both spellings — ::marker for
   engines that put it on the summary box, ::-webkit-details-marker for the
   older shape. */
.bn-timeline-card > summary {
  display: block;
  cursor: pointer;
  list-style: none;
}

.bn-timeline-card > summary::-webkit-details-marker { display: none; }
.bn-timeline-card > summary::marker { content: ""; }

.bn-timeline-card > summary:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
  border-radius: var(--bn-radius-sm);
}

/* WHEN → WHAT ABOUT → WHO, and the two ends are what the eye runs down. How
   long ago leads, because that is the thing a timeline is read for; the exact
   day sits beside it a step quieter for when the relative one is not precise
   enough; the record and the author are packed against the right-hand edge. */
.bn-timeline-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 1.25rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-timeline-age {
  flex: 0 0 auto;
  color: var(--bs-emphasis-color);
  font-weight: 500;
  white-space: nowrap;
}

/* The exact day, in the same ISO the date pickers and the group headings this
   replaced all used. Tertiary and tabular, so forty of them line up down the
   column without any of them asking to be read. */
.bn-timeline-date {
  flex: 0 0 auto;
  color: var(--bs-tertiary-color);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.bn-timeline-meta {
  margin-left: auto;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
}

/* The related records. Bare links on the head row rather than a third
   paragraph under the body, and each truncates rather than wrapping — a
   two-line head would undo the equal heights the rest of this block is for.
   The MARK stays, because a contact, a deal and a project are three different
   kinds in one line and the glyph varies with the row: that is an icon
   labelling the data, which is the only kind the app keeps. */
.bn-timeline-meta a {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
  max-width: 14rem;
  color: var(--bs-secondary-color);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-timeline-meta a:hover { color: var(--bs-emphasis-color); text-decoration: underline; }
.bn-timeline-meta .bn-i-xs { flex: 0 0 auto; color: var(--bs-tertiary-color); }
.bn-timeline-meta a:hover .bn-i-xs { color: inherit; }

/* WHO, AS A MARK RATHER THAN A NAME. `Kenan Blue North` was the widest thing
   on an entry and identical on nearly all of them, which is the same argument
   that put an avatar in the list pages' Owner column. The name is still there
   for a screen reader and in the title attribute. */
.bn-timeline-who {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
}

/* WHAT WAS SAID, IN ONE INK AND ONE WEIGHT. Emphasis colour, which is the
   page's own strongest text — white in dark mode — because this is the only
   thing on the entry anybody came to read and it was previously set a step
   below the lead line above it. One line until the card is opened. */
.bn-timeline-text {
  margin: 0.125rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--bs-emphasis-color);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.bn-timeline-card:not([open]) .bn-timeline-text {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* EDIT LIVES BEHIND THE FOLD. It used to be a pencil in the head row's end
   seat, revealed on hover, on every row of a forty-row history — one slip from
   retyping somebody's account of a phone call. Outside the <summary> it is
   only reachable from an entry somebody has deliberately opened, which is also
   the entry they can see all of. It keeps its word, because down here it has no
   line of its own to belong to and nothing beside it to say what it does. */
.bn-timeline-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.5rem;
}

.bn-timeline-edit {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0.1875rem 0.5rem;
  border: 0;
  border-radius: var(--bn-radius-sm);
  background-color: transparent;
  color: var(--bs-secondary-color);
  font-size: 0.75rem;
  transition: color 0.12s ease, background-color 0.12s ease;
}

.bn-timeline-edit:hover {
  background-color: var(--bn-nav-hover);
  color: var(--bn-on-primary);
}

.bn-timeline-edit:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

/* Trigger-written entries. A single line on the rail, with no block behind it —
   which is what tells them apart at a glance, rather than a dot four pixels
   smaller. There are more of these than of anything else on a busy pipeline.

   THEY GET AIR ON BOTH SIDES, which is the only thing separating one part of
   the history from the next now the month headings are gone: a run of stage
   changes reads as the rail ticking over between the things people did rather
   than as four more rows of the same list. Consecutive ones close up again —
   the space is between a system line and a card, not inside a run of them. */
.bn-timeline-system {
  align-items: center;
  margin-block: 0.625rem;
}

.bn-timeline-system + .bn-timeline-system { margin-top: 0; }
.bn-timeline-system:has(+ .bn-timeline-system) { margin-bottom: 0; }
.bn-timeline-item:first-child.bn-timeline-system { margin-top: 0; }
.bn-timeline-item:last-child.bn-timeline-system { margin-bottom: 0; }

.bn-timeline-system .bn-timeline-dot {
  margin-top: 0;
  width: 19px;
  height: 19px;
  margin-left: 4px;
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

.bn-timeline-system .bn-timeline-dot .bn-inline-icon,
.bn-timeline-system .bn-timeline-dot .bn-i-xs { width: 11px; height: 11px; }

.bn-timeline-line {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0.3125rem 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-timeline-said {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-timeline-meta time {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}


/* --- Due dates ------------------------------------------------------------
 *
 * The three tones a due date can take, shared by every surface that draws one:
 * /tasks' queue rows, My day's copy of them, and the record pages' Tasks tab,
 * which converged onto the same row. The bordered `.bn-task` card that used to
 * live here — a checkbox, a stacked body and a six-fragment meta line — is
 * gone with it; see "The queue" in CLAUDE.md.
 */

.bn-due-danger { color: var(--bs-danger); font-weight: 500; }
.bn-due-warning { color: var(--bn-on-warning); font-weight: 500; }

/* A pending start date. Tertiary, never a status tone — a task that has not
   started yet is not a warning, it is the schedule working. Only a DUE date can
   be late, which is why .bn-due has three tones and this has one. */

/* --- The Markdown body --------------------------------------------------- */

/* NOTHING RENDERS THIS TODAY, and that is worth writing down rather than
   leaving to be discovered. `fmt.taskBody` (which calls `fmt.markdown`) had one
   caller — the stacked body on the old `.bn-task` row — and all three task
   surfaces now show a description as a flag icon with the text in `title`, with
   the full body reachable in the edit modal's textarea. The formatter, its
   escape-first security model and this block are kept intact because they are
   one call site from being live again; `tasks.description` is still Markdown in
   the database and `.bn-md-input` below still styles the textarea it is written
   in. Delete all of it together, or not at all.

   The disclosure. A <details>, so there is no JavaScript to rebind after a
   dmxRouting view swap — the same reasoning as fmt.fields' empty-field fold. */
.bn-task-fold { margin: 0.125rem 0 0; }

.bn-task-fold > summary {
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
  cursor: pointer;
  list-style: none;
  border-radius: var(--bn-radius-sm);
}

/* The stock marker is replaced by a chevron of our own so it can sit at the
   FRONT of the line and rotate. `::-webkit-details-marker` is still needed
   alongside `list-style: none` for Safari. */
.bn-task-fold > summary::-webkit-details-marker { display: none; }

.bn-task-fold > summary::before {
  content: "";
  flex: 0 0 auto;
  width: 0.3125rem;
  height: 0.3125rem;
  margin-top: 0.3em;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
  color: var(--bs-tertiary-color);
}

.bn-task-fold[open] > summary::before { transform: rotate(45deg); }
.bn-task-fold > summary:hover .bn-task-desc { color: var(--bs-emphasis-color); }

/* The excerpt stays one line whether it is a summary or a plain paragraph, so a
   row's height never depends on how much somebody wrote. */
.bn-task-desc {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Rendered Markdown. Everything here is scoped to .bn-md, because these are the
   only bare `h4`/`ul`/`blockquote`/`pre` elements the app emits — every other
   surface is class-driven. */
.bn-md {
  margin: 0.4375rem 0 0.125rem;
  font-size: 0.75rem;
  line-height: 1.55;
  color: var(--bs-secondary-color);
}

.bn-md > :first-child { margin-top: 0; }
.bn-md > :last-child { margin-bottom: 0; }
.bn-md p { margin: 0 0 0.5rem; }

/* fmt.markdown starts headings at h4 — h1 is the page and h2 is the card, so a
   task body may not outrank the thing it sits inside. All three land on one
   size and separate by weight, since three heading sizes inside a list row
   would be a hierarchy nobody asked for. */
.bn-md h4,
.bn-md h5,
.bn-md h6 {
  margin: 0.6875rem 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
}

.bn-md ul,
.bn-md ol { margin: 0 0 0.5rem; padding-left: 1.125rem; }
.bn-md li { margin-bottom: 0.125rem; }

.bn-md blockquote {
  margin: 0 0 0.5rem;
  padding-left: 0.625rem;
  border-left: 2px solid var(--bs-border-color);
  color: var(--bs-tertiary-color);
}

.bn-md hr {
  margin: 0.625rem 0;
  border: 0;
  border-top: 1px solid var(--bs-border-color);
  opacity: 1;
}

.bn-md code {
  padding: 0.0625rem 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--bn-canvas);
  color: var(--bs-emphasis-color);
  font-size: 0.9em;
}

/* Wide content scrolls inside its own box rather than widening the row. A code
   block is the one thing in a task body with no wrap point of its own. */
.bn-md pre {
  margin: 0 0 0.5rem;
  padding: 0.5rem 0.625rem;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bn-canvas);
  overflow-x: auto;
}

.bn-md pre code { padding: 0; background: none; }
.bn-md a { color: var(--bs-primary); }

/* The composer's own textarea. Monospace because what goes in is markup, and a
   proportional font hides whether a list's dashes line up. */
.bn-md-input {
  font-family: var(--bs-font-monospace);
  font-size: 0.8125rem;
  line-height: 1.5;
}

/* --- File list ----------------------------------------------------------- */

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

.bn-file {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.75rem 0.875rem;
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-sm);
  margin-bottom: 0.5rem;
}

.bn-file-icon {
  width: 1.125rem;
  height: 1.125rem;
  flex: 0 0 auto;
  stroke-width: 1.75;
  color: var(--bs-secondary-color);
}

.bn-file-name { font-size: 0.8125rem; color: var(--bs-emphasis-color); }

.bn-file-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-file-meta > * { display: inline-flex; align-items: center; gap: 0.25rem; }

/* A quiet neutral pill, for labels that classify rather than signal state. */
.bn-pill-quiet {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

.bn-pill-info {
  background-color: var(--bn-fill-info);
  color: var(--bn-on-info);
}

/* ---------------------------------------------------------------------------
 * Phase 3 — pipeline
 *
 * The board itself (.bn-board, .bn-board-col, .bn-card, .bn-drop-active) was
 * written with the shell in Phase 1c and is unchanged. What follows is what the
 * real page needed on top of it: card interior, column header counts, the deal
 * stage bar and the stage-history list.
 * ------------------------------------------------------------------------ */

/* A page head that carries a view toggle as well as a button. */
.bn-page-head-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 0 0 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* --- Board columns -------------------------------------------------------- */

.bn-board-col-count {
  min-width: 1.375rem;
  padding: 0 0.375rem;
  border-radius: 999px;
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
  text-align: center;
  line-height: 1.25rem;
}

/* "N older than 90 days not shown" — Won and Lost are windowed, see
   app/api/deals/list.json. */
.bn-board-col-note {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin: 0.625rem 0.625rem 0;
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
}

/* --- Deal cards ----------------------------------------------------------- */

.bn-deal-card {
  cursor: grab;
  background-color: var(--bs-tertiary-bg);
  transition: background-color 120ms ease;
}

.bn-deal-card:active {
  cursor: grabbing;
}

/* Both board cards are the one .bn-card that is actually clickable/draggable,
   so a lift on hover is the affordance a flat, colour-matched card otherwise
   has no way to give — see the note above .bn-project-card, which already
   assumed this existed. */
.bn-deal-card:hover,
.bn-project-card:hover {
  background-color: var(--bn-card-hover);
}

.bn-card-sub {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* Pushes everything after it to the right of the meta row, so the value stays
   left and the avatar stays right however many chips sit between them. */
.bn-card-spacer {
  flex: 1 1 auto;
}

.bn-card-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.4375rem;
  border-radius: var(--bn-radius-pill);
  background-color: var(--bs-tertiary-bg);
  font-size: 0.6875rem;
  line-height: 1.4;
  white-space: nowrap;
}

.bn-card-chip .bn-i-xs { width: 0.75rem; height: 0.75rem; }

.bn-card-chip-warning {
  background-color: var(--bn-fill-warning);
  color: var(--bn-on-warning);
}

.bn-card-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--bs-border-color);
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-card-foot > * { display: inline-flex; align-items: center; gap: 0.25rem; }
.bn-card-foot .bn-i-xs { color: var(--bs-tertiary-color); }

/* A card's subtitle line — company, stage, whatever names the parent. */
.bn-card-sub-row {
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-card-sub-row .bn-i-xs { color: var(--bs-tertiary-color); }

/* --- The stage bar -------------------------------------------------------
 *
 * The lifecycle control on /deals/:id and /projects/:id — one track with the
 * open stages in it and the endings as caps beside it. See
 * views/partials/stage-bar.ejs for why the endings are not steps.
 *
 * IT WAS A ROW OF LOOSE CAPSULES. Every stage was its own 999px pill with a
 * border, a colour dot, a name and a percentage, and the current one was
 * filled solid `--bn-invert-bg` — seven of the loudest shape in the app,
 * wrapping wherever the stage names happened to add up, on the one control
 * whose whole job is to say which way the pipeline runs. Separate buttons
 * cannot say that; a track can.
 *
 * So the fill runs left to right. Behind you is a raised surface with a tick
 * on it, where you are is `--bn-nav-active` — the same 12% of primary the
 * sidebar's selected item and the board's drop target wear, because a
 * selection in this app is primary and never a neutral — and ahead of you is
 * bare track. Three states, each said by a fill AND by a mark, so neither
 * colour alone nor shape alone is carrying it.
 */

.bn-stagebar {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--bn-stack);
}

/* The recess the steps sit in — the same tone and border the filter bar's own
   segmented control uses, which is what a set of equal columns with one of
   them selected already looks like in this app.

   `1 1 22rem` rather than `1 1 auto`: the track takes the room, and below
   about 22rem it drops onto its own line instead of squeezing the caps, whose
   words do not truncate gracefully. */
.bn-stage-track {
  flex: 1 1 22rem;
  display: grid;
  grid-template-columns: repeat(var(--bn-stage-n, 4), minmax(0, 1fr));
  gap: 0.1875rem;
  padding: 0.1875rem;
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
}

.bn-stage-caps {
  display: flex;
  align-items: stretch;
  gap: 0.375rem;
}

.bn-step {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  min-width: 0;
  padding: 0.5rem 0.625rem;
  border: 1px solid transparent;
  border-radius: calc(var(--bn-radius) - 0.1875rem);
  background-color: transparent;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  line-height: 1.25;
  white-space: nowrap;
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

/* The name is the only thing here allowed to run out of room. A six-stage
   pipeline in a narrow column truncates a word; it does not push the caps off
   the end of the bar. */
.bn-step-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-step-mark { color: var(--bs-tertiary-color); }

/* Behind you: raised out of the recess, quiet, ticked. */
.bn-step.is-done {
  background-color: var(--bn-surface);
  color: var(--bs-secondary-color);
}

/* Where you are. The current step is rendered disabled — there is nothing to
   press, and it reads as "you are here" rather than as an option that does
   nothing. A step disabled for the moment a set_stage is in flight must NOT
   pick this up, or every press looks briefly like it already worked. */
.bn-step.active {
  background-color: var(--bn-nav-active);
  color: var(--bn-on-primary);
  font-weight: 500;
  opacity: 1;
}

.bn-step:hover:not(:disabled) {
  background-color: var(--bn-surface-hover);
  color: var(--bs-emphasis-color);
}

.bn-step:disabled:not(.active) {
  opacity: 0.5;
}

/* --- The end caps ---------------------------------------------------------
 *
 * Outside the track, because an ending is not a further step. Each is its own
 * button on the page canvas, drawn as a surface with an edge so it reads as
 * something to press rather than as another column of the track — and it
 * takes its tone only when it is hovered or when it is the state the record is
 * actually in. A permanently green Won and a permanently red Lost sitting on
 * every open deal is two alarms for something that has not happened. */
.bn-stage-cap {
  flex: 0 0 auto;
  padding: 0.5rem 0.875rem;
  background-color: var(--bn-surface);
  border-color: var(--bs-border-color);
}

.bn-stage-cap .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-stage-cap-success:hover:not(:disabled) {
  background-color: var(--bn-fill-success);
  border-color: var(--bn-edge-success);
  color: var(--bn-on-success);
}

.bn-stage-cap-danger:hover:not(:disabled) {
  background-color: var(--bn-fill-danger);
  border-color: var(--bn-edge-danger);
  color: var(--bn-on-danger);
}

.bn-stage-cap-success:hover:not(:disabled) .bn-i-xs { color: var(--bn-on-success); }
.bn-stage-cap-danger:hover:not(:disabled) .bn-i-xs { color: var(--bn-on-danger); }

/* The cap the record is currently in. It wins over `.bn-step.active` above by
   source order — a closed record's state is its ending, not the primary tint
   the track uses for "you are here". */
.bn-stage-cap.active {
  background-color: var(--bs-tertiary-bg);
  border-color: var(--bs-border-color);
  color: var(--bs-emphasis-color);
}

.bn-stage-cap-success.active {
  background-color: var(--bn-fill-success);
  border-color: var(--bn-edge-success);
  color: var(--bn-on-success);
}

.bn-stage-cap-danger.active {
  background-color: var(--bn-fill-danger);
  border-color: var(--bn-edge-danger);
  color: var(--bn-on-danger);
}

.bn-stage-cap-success.active .bn-i-xs { color: var(--bn-on-success); }
.bn-stage-cap-danger.active .bn-i-xs { color: var(--bn-on-danger); }

/* --- Inline add row, as used by the deal Contacts tab --------------------- */

.bn-inline-add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1.125rem;
}

.bn-inline-add .form-select {
  width: auto;
  min-width: 12rem;
}

/* Reconciled with the combobox above: .bn-sel's own base rule is width:100%,
   which is right inside a .row column and wrong inside this flex row — left
   alone it would claim the rest of the row's width and push the role select
   and the Add button onto a second line. */
.bn-inline-add .bn-sel {
  width: auto;
  min-width: 12rem;
}

.bn-w-role { width: 11rem !important; min-width: 11rem !important; }

/* --- The deal form's first-year readout ------------------------------------
   The arithmetic behind the headline figure, shown while the amounts are being
   typed. A recessed strip rather than another `.form-text`: it is not a hint
   belonging to one field, it is the RESULT of three of them, and it says
   something the fields cannot say individually. Quiet enough to ignore while
   typing a plain fixed-price deal, where it reads as one short line.

   `.bn-cf-total` is the retainer form's copy of the same thing — an amount and
   a billing period read together as MRR and ARR — and shares the rule rather
   than restating it, since it is the same claim in the same seat on the same
   kind of modal. It is a second selector rather than a rename because
   partials/deal-form.ejs and tools/browser_pass.py both already address the
   first name.

   `min-height` holds the row whether or not there is anything to say, so
   filling in an amount does not shunt the fields below it down the modal. */
.bn-df-total,
.bn-cf-total {
  min-height: 1.75rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.625rem;
  border-radius: var(--bn-radius-sm);
  background: var(--bn-canvas);
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

.bn-df-total:empty,
.bn-cf-total:empty {
  background: transparent;
}

/* --- Stage history -------------------------------------------------------- */

.bn-history {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.8125rem;
}

.bn-history li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5625rem 0;
  border-bottom: 1px solid var(--bs-border-color);
}

.bn-history li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.bn-history li:first-child { padding-top: 0; }

.bn-history-label {
  color: var(--bs-emphasis-color);
}

.bn-history .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-history time {
  margin-left: auto;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * Phase 4 — delivery
 *
 * The board, cards and stage bar are the Phase 3 pipeline's, unchanged — that
 * was the point of building them generically. What follows is what delivery
 * needed on top: a card whose title is a link rather than the whole card being
 * one, the health control in its three tones, and the retainer row's actions.
 * ------------------------------------------------------------------------ */

/* A project card is an <article>, not an <a>, because it contains a form
   control — invalid inside a link, and a trap for keyboard users. So the
   TITLE'S LINK IS STRETCHED OVER THE WHOLE CARD instead: one real anchor with
   an absolutely positioned overlay, which is what makes pressing anywhere on
   the card open the project the way pressing anywhere on a deal card does.

   A stretched link rather than a click handler, for the reason every other
   "the markup is the switch" call in this file gives: there is nothing to
   rebind after a dmxRouting view swap, the URL is in the status bar, and
   middle-click still opens a tab. */
.bn-project-card {
  position: relative;
  cursor: grab;
  background-color: var(--bs-tertiary-bg);
  transition: background-color 120ms ease;
}

.bn-project-card:active {
  cursor: grabbing;
}

a.bn-card-open {
  display: block;
  color: var(--bs-emphasis-color);
  text-decoration: none;
}

.bn-project-card .bn-card-open::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* AND THE TITLE NO LONGER ANSWERS FOR ITSELF. It used to turn primary and
   underline on hover, which was right while it was the one live target on the
   card and is wrong now that the whole card is: the card fills, exactly as a
   deal card does, and a second thing lighting up inside it would say the title
   was a different destination from the surface around it. */

/* --- Health -------------------------------------------------------------- */

/* On the board card, as its own row under the footer's meta line.

   The control itself is partials/form-priority at `.bn-prio-sm` — the same
   slider the task modals use for priority, which is what a three-value scale
   with an obvious order and a glyph per step is for. It replaced a
   `.bn-health-select` pill: a <select> styled as a coloured oval, whose tone
   was a `bn-health-*` class the board's JavaScript rewrote by regex on every
   save, and which said "On track" without ever hinting that two worse answers
   existed until it was opened.

   A row of its own rather than a fourth item in `.bn-card-foot`. The old pill
   was the width of a word and sat happily in that flex line; a track is the
   width of the card and would push the two dates onto a second row at a width
   that depends on the stage name. It is also the one thing on this card you can
   DO rather than read.

   It is also the one thing on the card that has to sit ABOVE the title's
   stretched overlay, which is what `position: relative; z-index: 1` is for.
   Everything else on a project card is text, and text under the overlay is
   text you can press to open the project — which is the point. */
.bn-card-health {
  position: relative;
  z-index: 1;
  margin-top: 0.5rem;
}

/* ON THE RECORD PAGE IT IS THE SAME SLIDER, in the rail rather than in a
   full-width strip of its own under the stage bar. That strip was a bordered
   band holding one control, one word and a due date the rail's Details rows
   already carried — and the control inside it was a `.bn-segmented`, the one
   place in the app where health was drawn as three words rather than as the
   three marks the board and the project modal both use. One control, one
   shape, three surfaces. */

/* --- Retainers ----------------------------------------------------------- */

/* An END DATE inside 90 days. Deliberately not `warning`: nothing is wrong, it
   is a date worth acting on before it arrives.

   It was `.bn-renewal-soon`, at 60 days, until migration 025 dropped
   contracts.start_date and the derived renewal it anchored. An agreement's
   ending is the one date left and the only one that carries a consequence,
   so it inherits the tone the anniversary was wearing. */
.bn-ending-soon {
  color: var(--bn-on-warning);
  font-weight: 600;
}

/* A `display: flex` <td> IS NO LONGER A TABLE CELL. It stops participating in
   the row's height, so on a retainer row two lines tall the actions cell stayed
   65px against the row's 91 — and its border-bottom drew 26px above every other
   cell's, as a rule that stopped short of the right-hand end of the row. The
   cell stays a table-cell; the flex row is an inner wrapper. */
.bn-row-actions { white-space: nowrap; }

.bn-row-actions > .bn-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.25rem;
}

/* Wide enough for "Cancelled", which is the longest option. It was 8.5rem,
   picked before the row lost its two button labels.

   It sizes the COMBOBOX WRAPPER now, not a .form-select — the row's status
   control is partials/form-select, the same one the retainer modal's own
   Status field is. `.bn-sel` is width:100% and would otherwise take the whole
   cell, so this is still the one thing deciding the field's width; the trigger
   inside takes the small metrics the activity composer's date trigger already
   asks for the same way, since `.form-select-sm` is not a class it wears.
   `align: 'right'` at the call site is what keeps the panel inside the page:
   this is the last control on the row. */
.bn-w-status { width: 7rem !important; min-width: 7rem !important; }
.bn-w-status .bn-sel-trigger {
  --bs-form-select-padding-y: 0.25rem;
  --bs-form-select-padding-x: 0.5rem;
  font-size: 0.8125rem;
  gap: 0.375rem;
}

/* --- Plain list, as used by the project record's people panel ------------- */

.bn-plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.8125rem;
}

.bn-plain-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4375rem 0;
}

.bn-plain-list .bn-i-sm,
.bn-plain-list .bn-i-xs { color: var(--bs-tertiary-color); }

/* Same tone as the warning chip on a deal card, for a project that is late. */
.bn-card-chip-danger {
  background-color: var(--bn-fill-danger);
  color: var(--bn-on-danger);
}

/* ---------------------------------------------------------------------------
 * Phase 5 — global search and My day
 *
 * The search panel and the dashboard's work lists. Nothing here is a --bs-*
 * override: the Theme tab owns those and this file loads after Bootstrap, so
 * redefining one would silently outrank the theme build (CLAUDE.md, Theme).
 * ------------------------------------------------------------------------ */

/* --- Global search -------------------------------------------------------- */

/* IT IS A DIALOG NOW, not a field in the bar. What it always was, functionally,
 * is a command palette — summoned with CTRL-K or the topbar's search icon,
 * answered, and dismissed — and a palette that is permanently open is a 440px
 * input holding the widest seat on every page in the app for the few times a
 * day anyone uses it. The markup is in partials/topbar.ejs, below the header.
 *
 * The overlay is `display: grid` with `place-items: center`, which is the whole
 * of the centring. `hidden` needs restating as `display: none` because a class
 * rule beats the UA stylesheet's own [hidden] rule and this one sets display.
 */
.bn-search-modal {
  position: fixed;
  inset: 0;
  z-index: 1080;
  display: grid;
  place-items: center;
  padding: 1rem;
}

.bn-search-modal[hidden] { display: none; }

/* The scroll lock. On <html>, not <body> — Bootstrap's own modals write inline
   styles onto body, and two things overwriting one style attribute is how a
   page ends up permanently unscrollable. */
.bn-search-open { overflow: hidden; }

/* The backdrop darkens the page, which is the point of the whole gesture: the
   app is still there and still readable, and nothing else is live. A literal
   rgba rather than a token, because it is a scrim over an unknown surface and
   has to be the same weight in both colour modes — it darkens what is behind
   it rather than being a colour of its own. */
.bn-search-backdrop {
  position: fixed;
  inset: 0;
  background-color: rgba(9, 9, 11, 0.55);
  animation: bn-search-fade 0.12s ease-out;
}

/* THE DIALOG'S OWN BOX IS THE FIELD AND NOTHING ELSE, which is what keeps the
   centring still. .bn-search-panel below is absolutely positioned, so results
   hang beneath the dialog instead of growing it — a centred box that grew
   downward would push the input you are typing in upward on every keystroke,
   which is the one thing a search field must never do. */
.bn-search-dialog {
  position: relative;
  width: min(38rem, 100%);
  animation: bn-search-in 0.12s ease-out;
  margin-bottom: 18rem;
}

.bn-search-dialog .bn-search {
  flex: 1 1 auto;
  width: 100%;
}

/* Bigger, taller and on a raised surface rather than the recessed grey it was
   in the bar. In the topbar it was one control among several and had to recede;
   here it is the only thing on screen that is not dimmed, and it is being typed
   into the instant it appears. */
.bn-search-dialog .bn-search input {
  padding: 1rem 4.5rem 1rem 3.25rem;
  font-size: 1.0625rem;
  background-color: var(--bn-surface);
  border-color: var(--bs-border-color);
  border-radius: var(--bn-radius);
  box-shadow: var(--bn-lift-lg);
}

.bn-search-dialog .bn-search input:focus {
  border-color: var(--bs-primary);
  box-shadow: var(--bn-lift-lg), 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

.bn-search-dialog .bn-search > i[data-lucide],
.bn-search-dialog .bn-search > svg {
  left: 1.1875rem;
  width: 1.25rem;
  height: 1.25rem;
}

/* The hint says ESC, not the shortcut that opened this — the way in has been
   used by the time anyone can read it, and the way out is the thing worth
   saying. It stays put while typing, unlike the topbar's CTRL-K hint, because
   this field is wide enough that it never sits over the text. */
.bn-search-kbd {
  position: absolute;
  right: 1rem;
  top: 1.125rem;
  padding: 0.125rem 0.4375rem;
  border: 1px solid var(--bs-border-color);
  border-radius: 0.25rem;
  background-color: var(--bn-canvas);
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
  pointer-events: none;
}

.bn-search-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  right: 0;
  z-index: 1030;
  /* Capped against the VIEWPORT as well as at a fixed height, because this
     hangs from the middle of the screen rather than from a bar at the top —
     28rem of results below a centred field would run off the bottom. */
  max-height: min(28rem, 34vh);
  overflow-y: auto;
  padding: 0.5rem;
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
  box-shadow: var(--bn-lift-lg);
}

@keyframes bn-search-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes bn-search-in {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .bn-search-backdrop,
  .bn-search-dialog { animation: none; }
}

/* The group heading carries the kind's icon — the same one the sidebar uses for
   that record type, so a result set reads as "these came from Companies" without
   the label having to be shouted in uppercase. */
.bn-search-group {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.625rem 0.625rem 0.375rem;
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-search-group-label {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-weight: 600;
}

.bn-search-group-label .bn-i-xs { color: var(--bs-tertiary-color); }
.bn-search-group-count { color: var(--bs-tertiary-color); }

.bn-search-hit {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4375rem 0.625rem;
  border-radius: var(--bn-radius-sm);
  color: var(--bs-body-color);
  text-decoration: none;
}

/* Hover and the keyboard highlight are the same state on purpose — the arrow
   keys should look exactly like pointing at a row.

   Primary-tinted rather than the flat `--bs-tertiary-bg` it used to be, for the
   same reason the nav links changed with it: a highlight is a selection, and
   every other selection in this app is a step of primary mixed into whatever it
   is sitting on. --bn-surface-hover rather than --bn-nav-hover, because a hit
   sits on a card surface and not on chrome. */
.bn-search-hit:hover,
.bn-search-hit.active {
  background-color: var(--bn-surface-hover);
  color: var(--bs-body-color);
}

.bn-search-hit.active .bn-search-hit-title { color: var(--bn-on-primary); }

.bn-search-hit-title {
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-search-hit-sub {
  flex: 1;
  min-width: 0;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bn-search-hit-meta {
  flex: 0 0 auto;
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
}

.bn-search-none {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 1rem 0.75rem;
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.bn-search-none .bn-i { color: var(--bs-tertiary-color); }

/* --- My day --------------------------------------------------------------- */

/* A tile that navigates. The tiles on the record pages are plain divs; these
   are anchors, so they need the link colours undone and an affordance adding. */
a.bn-tile {
  display: flex;
  flex-direction: column;
  align-items: space-between;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}

a.bn-tile:hover {
  background-color: var(--bn-surface-hover);
}

/* The affordance is the corner arrow, not the headline — the value is data and
   should not change colour just because a pointer is nearby; the arrow is
   already the "this goes somewhere" mark, so it is the one that answers a
   hover.

   The transition is declared on the icon rather than on the tile, because the
   property changing belongs to the icon: a `transition` only animates a change
   on the element that carries the declaration, so listing `color` up in
   a.bn-tile would animate the tile's own text and leave the arrow snapping. */
a.bn-tile .bn-tile-label .bn-i-sm {
  /* The corner arrow, and the one icon on a tile label that is pushed to the
     far end of the line rather than sitting at the head of it. Only a tile that
     navigates has one. */
  margin-left: auto;
  transition: color 0.12s ease;
}

a.bn-tile:hover .bn-tile-label .bn-i-sm {
  color: var(--bs-primary);
}

/* The three attention lists, stacked in the narrow column. */
.bn-day-side {
  display: flex;
  flex-direction: column;
  gap: var(--bn-stack-sm);
}

/* The heading of a work list: an icon tile, a title, a count. Same shape as
   .bn-card-heading, one size up, because on My day these ARE the page. */
.bn-worklist-head {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 1rem;
}

.bn-worklist-head h2 {
  font-size: 0.9375rem;
  margin: 0;
  color: var(--bs-emphasis-color);
}

.bn-worklist-head .bn-board-col-count { margin-left: auto; }

.bn-worklist-sub {
  margin: 0 0 1.125rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* .bn-worklist-group — the flat "Overdue · 3" heading with a hairline running
   to the end of the row — is GONE. My day's task buckets were its only caller,
   and they are <details> now: /tasks' .bn-tq-head, which carries the same word
   and count and folds the group with no JavaScript to rebind after a view
   swap. The three attention lists below never grouped anything. */

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

.bn-work-item {
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  padding: 0.75rem 0;
  border-top: 1px solid var(--bs-border-color);
}

.bn-work-item:first-child {
  border-top: 0;
  padding-top: 0;
}

.bn-work-item:last-child { padding-bottom: 0; }

.bn-work-main {
  flex: 1;
  min-width: 0;
}

.bn-work-title {
  display: block;
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
  text-decoration: none;
}

.bn-work-title:hover {
  color: var(--bs-primary);
}

.bn-work-sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem 0.5rem;
  margin-top: 0.125rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-work-sub > * { display: inline-flex; align-items: center; gap: 0.25rem; }
.bn-work-sub .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-work-sub a {
  color: inherit;
  text-decoration: none;
}

.bn-work-sub a:hover {
  color: var(--bs-primary);
}

/* The right-hand column of a work row: a number above, a metric below, both
   right-aligned so the eye can scan one column of figures. */
.bn-work-side {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.125rem;
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
  text-align: right;
}

.bn-work-metric {
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}

/* An empty attention list is a sentence, not a 3rem-tall .bn-empty block —
   three of them stacked would push the page off the screen saying nothing. */
.bn-worklist-empty {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  margin: 0;
  padding: 0.875rem 1rem;
  border: 1px dashed var(--bs-border-color);
  border-radius: var(--bn-radius-sm);
  background-color: var(--bs-secondary-bg);
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-worklist-empty .bn-i { color: var(--bs-tertiary-color); margin-top: 0.0625rem; }

/* My day's task list IS the queue's, narrowed — .bn-tq-group, .bn-tq-row and
   the one-circle mark from "The queue" in CLAUDE.md, sitting on the page
   canvas rather than inside a card.

   THE CARD IS GONE, AND THAT IS WHY THERE IS ALMOST NOTHING TO OVERRIDE HERE.
   The section used to be a .bn-card, which forced every row to give up its own
   fill: .bn-tq-row's background is the SAME token as a card's, so a row drawn
   inside one is a block on a block that says nothing, and this file used to
   carry a transparent-row-plus-border-separator rule to work around it. With
   the container removed the rows are back on the canvas and the queue's own
   fill and 2px gap do exactly what they do on /tasks — one list, one shape,
   two pages.

   What is left is the three seats' widths, because this column is roughly 3/5
   of the content area where /tasks has all of it. */

/* THE ADD BUTTON, AT THE FAR END OF THE FIRST GROUP'S LINE. My tasks has no
   heading of its own any more — see the note in my-day.ejs — so the one thing
   that head was holding which nothing else could, the way in to the add form,
   sits on the first .bn-tq-head instead.

   A plus and nothing else: no label, no fill, no border, and a fill on hover.
   That is deliberately the SAME language as .bn-tq-edit, the pencil at the end
   of a /tasks row — one shape for "an icon-only control that belongs to the
   line it sits on", so a second one is recognisable without being explained.
   It is a class of its own rather than a .btn variant because a .btn brings a
   whole --bs-btn-* box model to a control that wants none of it.

   `margin-left: auto` is what puts it at the end: .bn-tq-head is a flex row,
   and the caret, the word and the count all sit at the near end of it. */
.bn-tq-add {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  margin-left: auto;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--bn-radius-sm);
  color: var(--bs-tertiary-color);
  transition: background-color 0.12s ease, color 0.12s ease;
}

.bn-tq-add:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-primary);
}

/* The summary's own :hover turns the word and the caret primary, and without
   this the button would follow them — so a pointer anywhere on the line would
   light up a control it is not over. It takes its colour from its own hover
   only. */
.bn-tq-head:hover .bn-tq-add { color: var(--bs-tertiary-color); }
.bn-tq-head:hover .bn-tq-add:hover { color: var(--bs-primary); }

.bn-tq-add:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

/* Priority is a bare glyph here (.bn-pill-icon — the word moved to `title`),
   so its seat shrinks to the mark. The due seat loses the width /tasks needs
   for "N days overdue", which dueLabelShort() in my-day.ejs cuts to "N d
   overdue"; the record seat gives up what a full company name would take. */
#day-tasks .bn-tq-prio { flex-basis: 1.5rem; justify-content: center; }
#day-tasks .bn-tq-due { flex-basis: 6.5rem; }
#day-tasks .bn-tq-rec { flex-basis: 8.5rem; }

/* AND IT NARROWS IN A DIFFERENT ORDER, so it declares its own container rather
   than answering /tasks' breakpoints.

   Those are tuned for a row with five seats on a full-width page, and they
   drop the record first — at 46rem, which this column is under at every window
   width above the point .bn-two-col collapses. The record is the one seat this
   list is least willing to lose: it carries the company's favicon, which is
   the mark that company gets everywhere else in the app, and there is no
   assignee seat here competing for the room (every row is mine by definition).
   So priority goes first and the record last, and both thresholds are set
   against the width this column actually gets — measured, not guessed, the
   same lesson the chart rows taught. */
#day-tasks .bn-tq-group { container-name: bn-tq-day; }

@container bn-tq-day (max-width: 24rem) {
  #day-tasks .bn-tq-prio { display: none; }
}

@container bn-tq-day (max-width: 19rem) {
  #day-tasks .bn-tq-rec { display: none; }
}

/* --- The queue on a record page ------------------------------------------
 *
 * THE THIRD COPY, and the one that drops the RECORD seat rather than the
 * assignee's. partials/task-list draws these rows on a company, contact, deal
 * or project, where the record is the page you are standing on: a column of
 * the same favicon forty times is a heading, and the heading is the tab. Who
 * has it is the seat that survives instead, because delivery work on a project
 * is the one task list in the app that is routinely somebody else's.
 *
 * It sits in the left column of `.bn-record-grid`, so it is narrower than
 * /tasks' full width and wider than My day's 3/5 — hence its own container and
 * its own two thresholds rather than either page's. Priority goes first, since
 * the due date and the owner are what a delivery list is read for. */
#record-tasks .bn-tq-prio { flex-basis: 6rem; }
#record-tasks .bn-tq-due  { flex-basis: 8.5rem; }
#record-tasks .bn-tq-who  { flex-basis: 1.75rem; }

#record-tasks .bn-tq-group { container-name: bn-tq-record; }

@container bn-tq-record (max-width: 30rem) {
  #record-tasks .bn-tq-prio { display: none; }
}

@container bn-tq-record (max-width: 22rem) {
  #record-tasks .bn-tq-due { flex-basis: 6rem; }
  #record-tasks .bn-tq-who { display: none; }
}

/* ---------------------------------------------------------------------------
 * Phase 6 — settings ladders, tags, and the task queue
 * ------------------------------------------------------------------------ */

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

/* Two panels side by side above 1200px, stacked below. Both settings pages
   show two lists of the same shape, and reading them as one column loses the
   point that they are separate ladders. */
.bn-stage-panels {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--bn-gutter);
}

@media (min-width: 1200px) {
  .bn-stage-panels { grid-template-columns: 1fr 1fr; }
}

.bn-panel {
  background-color: var(--bn-surface);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius-card);
  padding: var(--bn-pad);
}

.bn-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

/* An icon tile, then the title and its description. Both settings pages show
   two panels of the same shape side by side, and the tile is what lets you tell
   at a glance which ladder you are looking at. */
.bn-panel-ident {
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  min-width: 0;
}

.bn-panel-head h2 {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--bs-emphasis-color);
}

.bn-panel-sub {
  margin: 0.25rem 0 0;
  max-width: 46ch;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* A one-line "there is nothing here, and this is why" inside a panel. Too small
   for the full .bn-empty treatment — several of these sit inside a card that
   already has a heading — but the icon is what stops it reading as a stray
   sentence someone forgot to delete. The text is wrapped in a <span> at every
   call site because these sentences contain links, and without the wrapper each
   <a> becomes its own flex item and the line breaks apart. */
.bn-quiet-line {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin: 0;
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.bn-quiet-line .bn-i-sm {
  margin-top: 0.1875rem;
  color: var(--bs-tertiary-color);
}

/* The explanatory note at the foot of a settings page, and under the file list.
   These are the paragraphs that say WHY a page works the way it does — a retired
   stage keeps its rows, a deleted tag is the one real delete — and as loose grey
   text under a table they read as a footnote nobody has to finish. A recessed
   strip with an info mark makes them part of the page. */
.bn-page-note {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: var(--bn-stack-sm) 0 0;
  padding: 0.875rem 1.125rem;
  background-color: var(--bs-secondary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bn-radius);
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  max-width: 88ch;
}

.bn-page-note .bn-i {
  margin-top: 0.0625rem;
  color: var(--bs-tertiary-color);
}

/* --- Settings ladder rows ------------------------------------------------ */

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

.bn-stage-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6875rem 0.25rem;
  border-top: 1px solid var(--bs-border-color);
}

.bn-stage-row:first-child { border-top: 0; }

/* A retired stage is still readable — it has to be, to be brought back — but it
   reads as switched off rather than merely secondary. */
.bn-stage-retired { opacity: 0.55; }

/* The reorder pair. They were `&uarr;` / `&darr;` glyphs, which at 12px in a
   zero-padding button were two faint smudges; as Lucide chevrons they need a
   real hit area to go with them, so each is a 1.25rem square. */
.bn-stage-move {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  line-height: 1;
}

.bn-stage-move .btn {
  --bs-btn-padding-y: 0;
  --bs-btn-padding-x: 0;
  display: grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 0.25rem;
  color: var(--bs-tertiary-color);
  text-decoration: none;
}

.bn-stage-move .btn:hover {
  color: var(--bs-primary);
  background-color: var(--bs-secondary-bg);
}

.bn-stage-move .btn:disabled { opacity: 0.25; }
.bn-stage-move .bn-i-xs { width: 0.875rem; height: 0.875rem; stroke-width: 2; }

/* The settings ladder's own, one size up. `display` is inherited from the rule
   in the Kanban section above — see the note there about inline-block. */
.bn-stage-dot {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

.bn-stage-main { flex: 1; min-width: 0; }

.bn-stage-name {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
}

.bn-stage-sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem 0.625rem;
  margin-top: 0.1875rem;
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-stage-sub > * { display: inline-flex; align-items: center; gap: 0.25rem; }
.bn-stage-sub .bn-i-xs { color: var(--bs-tertiary-color); }

.bn-stage-sub code {
  font-size: inherit;
  color: inherit;
}

.bn-stage-prob {
  flex: 0 0 auto;
  min-width: 4.5rem;
  text-align: right;
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
}

.bn-stage-count {
  flex: 0 0 auto;
  min-width: 5.5rem;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.0625rem;
  font-size: 0.75rem;
}

.bn-stage-ever {
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-stage-actions { flex: 0 0 auto; }

.bn-stage-actions .btn {
  --bs-btn-padding-y: 0;
  --bs-btn-padding-x: 0.25rem;
  text-decoration: none;
}

/* --- Task queue --------------------------------------------------------- */

/* The bucket chips are filters that show their own count, so they are links
   rather than a segmented control — each one is a URL worth sharing. */
.bn-bucket-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--bn-stack-sm);
}

.bn-bucket-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.375rem 0.8125rem;
  border-radius: 999px;
  border: 1px solid var(--bs-border-color);
  background-color: var(--bn-surface);
  font-size: 0.75rem;
  color: var(--bs-body-color);
  text-decoration: none;
}

.bn-bucket-chip span {
  font-variant-numeric: tabular-nums;
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
}

.bn-bucket-chip:hover { border-color: var(--bs-secondary-color); }

.bn-bucket-chip.active {
  background-color: var(--bn-invert-bg);
  border-color: var(--bn-invert-bg);
  color: var(--bn-invert-fg);
}

.bn-bucket-chip.active span { color: var(--bn-invert-fg-dim); }

.bn-bucket-danger:not(.active) { color: var(--bs-danger); }
.bn-bucket-warning:not(.active) { color: var(--bn-on-warning); }
.bn-bucket-quiet:not(.active) { color: var(--bs-secondary-color); }

/* --- The queue: groups, rows, the mark ------------------------------------
 *
 * /tasks only. The record pages and My day still draw .bn-task, which is the
 * bordered card with a stacked body this replaced — they get their own pass.
 *
 * THE ROW IS A LIST ROW WITH FIXED SEATS. Every cell has a flex-basis rather
 * than being sized by its content, which is what makes a column of due dates or
 * a column of avatars scan straight down forty rows without any of it being a
 * table. The bases are set here and not in a viewport media query: a media
 * query cannot see the content area's own width, which is the bug that once
 * collapsed the chart bars to zero inside a third-width card.
 *
 * AND IT HAS NO BORDER. Each row is still its own surface block — the card the
 * old design had — but the outline is gone and a 2px gap does the separating.
 * Forty outlined rectangles is a lot of ink to say "these are forty items".
 */

.bn-tq-group { margin-bottom: var(--bn-stack); }

/* A <details> summary. `list-style: none` plus the -webkit- rule is what it
   takes to be rid of the stock marker in every engine; the caret below is ours
   so it can sit at the front and rotate, exactly as .bn-task-fold's does. */
.bn-tq-head {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0 0 0.5rem 0.25rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.bn-tq-head::-webkit-details-marker { display: none; }

.bn-tq-caret {
  flex: 0 0 auto;
  color: var(--bs-tertiary-color);
  transition: transform 0.15s ease;
}

.bn-tq-group[open] > .bn-tq-head .bn-tq-caret { transform: rotate(90deg); }

.bn-tq-head-label {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--bs-emphasis-color);
}

/* The count is a number, not a badge. A filled pill here would be a fifth
   tinted thing on a line that already carries a tone on two of the six groups,
   and the chips above are where a count is a control. */
.bn-tq-head-count {
  font-size: 0.6875rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--bs-tertiary-color);
}

.bn-tq-head:hover .bn-tq-head-label { color: var(--bs-primary); }
.bn-tq-head:hover .bn-tq-caret { color: var(--bs-primary); }

/* Two of the six groups say something about time by their name alone, so the
   name carries the tone. Overdue is the only one that is a problem. */
.bn-tq-head-danger .bn-tq-head-label { color: var(--bs-danger); }
.bn-tq-head-warning .bn-tq-head-label { color: var(--bn-on-warning); }
.bn-tq-head-quiet .bn-tq-head-label { color: var(--bs-secondary-color); }

.bn-tq {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bn-tq-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.75rem;
  padding: 0.4375rem 0.875rem;
  background-color: var(--bn-surface);
  border-radius: var(--bn-radius-sm);
  transition: background-color 0.12s ease;
}

.bn-tq-row:hover { background-color: var(--bn-surface-hover); }

/* IN PROGRESS IS THE ONE STATE THE ROW ITSELF CARRIES, because it is the one
   that is about somebody else: it says this is being worked on right now. An
   inset shadow rather than a border-left, so the row's own box is untouched and
   the accent cannot add a pixel to the height or fight the radius. */
.bn-tq-row-working {
  background-color: color-mix(in srgb, var(--bs-info) 6%, var(--bn-surface));
  box-shadow: inset 2px 0 0 var(--bs-info);
}

.bn-tq-row-working:hover {
  background-color: color-mix(in srgb, var(--bs-info) 10%, var(--bn-surface));
}

.bn-tq-row-done .bn-tq-name {
  text-decoration: line-through;
  color: var(--bs-secondary-color);
}

.bn-tq-row-done { opacity: 0.72; }
.bn-tq-row-done:hover { opacity: 1; }

/* --- The mark -------------------------------------------------------------
 *
 * One circle, three looks and two gestures. The ring is a box-shadow rather
 * than a border so the button's box is exactly its size at every state and
 * nothing shifts by a pixel when it fills.
 */
.bn-tq-mark {
  position: relative;
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  /* A press of any length must not select the title next to it or start a
     drag, on any input. */
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.bn-tq-mark-face {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--bs-tertiary-color);
  transition: box-shadow 0.12s ease, background-color 0.12s ease;
}

.bn-tq-mark:hover .bn-tq-mark-face { box-shadow: inset 0 0 0 1.5px var(--bs-primary); }

.bn-tq-mark:focus-visible {
  outline: none;
}

/* In progress: half filled, which is the shape the state already has a symbol
   for. A gradient rather than a second element, so the face stays one node. */
.bn-tq-row-working .bn-tq-mark-face {
  box-shadow: inset 0 0 0 1.5px var(--bs-info);
  background-image: linear-gradient(to right, var(--bs-info) 50%, transparent 50%);
}

/* Done: filled, with the check on top. */
.bn-tq-row-done .bn-tq-mark-face {
  box-shadow: inset 0 0 0 1.5px var(--bs-success);
  background-color: var(--bs-success);
}

/* The check is in the markup on every row and CSS decides whether it shows —
   nothing assigns to this button's textContent, which is what would wipe an
   icon until the next reload. */
.bn-tq-mark-check {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  opacity: 0;
  pointer-events: none;
}

.bn-tq-row-done .bn-tq-mark-check { opacity: 1; }

/* THE HOLD. --bn-hold is written by requestAnimationFrame as a plain number,
   0 to 100, so the sweep needs no @property registration and no CSS animation
   to cancel — releasing simply stops writing it and removes the property. The
   ring is drawn OUTSIDE the circle so it never covers the face underneath it,
   and the mask is what turns a filled conic wedge into a 2px arc. */
.bn-tq-mark::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  opacity: 0;
  background: conic-gradient(var(--bs-success) calc(var(--bn-hold, 0) * 1%), transparent 0);
  /* CLOSEST-SIDE, not the default. A bare `circle` sizes to farthest-CORNER,
     which puts the gradient's 100% out at the box's diagonal — so the arc lands
     at a radius smaller than the face and is painted underneath it, invisible.
     closest-side pins 100% to the box's own edge, which is where the ring is. */
  -webkit-mask: radial-gradient(circle closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(circle closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  pointer-events: none;
  transition: opacity 0.8s cubic-bezier(.84,.01,.58,.29)
}

.bn-tq-mark.is-holding::after { opacity: 1; }

/* And the circle itself leans toward where it is going, so the gesture reads
   as "completing" before the ring has closed. */
.bn-tq-mark.is-holding .bn-tq-mark-face {
  box-shadow: inset 0 0 0 1.5px var(--bs-success);
}

.bn-tq-mark.is-busy { opacity: 0.5; }
.bn-tq-mark[disabled] { cursor: default; }

/* --- The cells ------------------------------------------------------------ */

.bn-tq-title {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.8125rem;
  color: var(--bs-emphasis-color);
}

.bn-tq-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A description, a parent, a count of open subtasks: three things a row can say
   about itself that are worth a glyph and not worth a line. Each carries its
   own text in `title`, which is the whole of the description's presence on this
   page now — the fold that used to print an excerpt under the title was a
   second row of height on some rows and nothing on others. */
.bn-tq-flag {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.1875rem;
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  color: var(--bs-tertiary-color);
  cursor: default;
}

.bn-tq-flag:hover { color: var(--bs-secondary-color); }

.bn-tq-cell {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  min-width: 0;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-tq-cell .bn-i-xs { color: var(--bs-tertiary-color); }

/* The priority pill's icon is the pill's own tone, not the cell's tertiary
   grey the rule above sets for everything else — so a low-priority pill's
   chevron reads the same quiet grey as its label and an urgent one's flame
   reads the same red.

   THE RADIUS OVERRIDE IS GONE, and its argument won everywhere instead. It was
   `--bn-radius-sm` against the rest of the app's 999px, on the reasoning that a
   scale like priority sits more like a step than a stamp; the table pass then
   took 999px off `.bn-pill` altogether for the same reason, and `--bn-radius-pill`
   is tighter than the override was. Left in place it would make this the one
   ROUNDER pill in the app — the override arguing the opposite of what it was
   written to argue. */
.bn-tq-prio .bn-pill .bn-i-xs { color: inherit; }

.bn-tq-prio { flex-basis: 6.5rem; }
.bn-tq-due  { flex-basis: 9.5rem; }
.bn-tq-rec  { flex-basis: 12rem; }
.bn-tq-who  { flex-basis: 1.75rem; justify-content: center; }
.bn-tq-act  { flex-basis: 2rem; justify-content: flex-end; }

/* The due fragment is its own flex box, because the cell's gap only separates
   the cell's own children and .bn-due is one of them — without this the clock
   sits hard against the words. And its icon takes the fragment's tone, or the
   tertiary grey above wins on specificity and an overdue task gets a neutral
   clock beside red words: the same fix .bn-task-meta needed. */
.bn-tq-due .bn-due,
.bn-tq-quiet {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  min-width: 0;
  /* Both of the table pass's date-cell rules, for the same two reasons. A date
     that wraps mid-value reads as a rendering fault rather than as a wrap —
     "3 d overdue" breaking after the number is the one that bites here — and
     tabular figures are what let a column of "12 d overdue" and "3 d overdue"
     line up down forty rows. */
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.bn-tq-due .bn-due .bn-i-xs { color: inherit; }

/* AND THE TONE STAYS `--bs-danger`, which looks like the odd one out and is not.
   Every pill and chip in the app takes `--bn-on-*` — but those are inks for a
   FILLED shape, sat on a 12% wash of their own hue, and this is bare text on
   the row's own surface. Measured against white: `--bs-danger` (#bf423a) is
   5.18:1 and `--bn-on-danger` (#FF5C56) is 3.04:1, so "matching the pills" here
   would have taken the most urgent line on the page down to a ratio a 12px
   label does not own. Same reason `.bn-due-warning` is left alone. */

.bn-tq-rec a {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--bs-body-color);
  text-decoration: none;
}

/* The same answer a link in a table cell gives — body ink, and the underline is
   what says it is a link. Primary here made the one indigo thing on a row whose
   own title, the thing that actually wants the eye, is plain emphasis ink. */
.bn-tq-rec a:hover {
  color: var(--bs-emphasis-color);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* The modal footer's Archive. A link button, but not underlined — every other
   link-styled control in this app drops the underline, and a red underlined
   phrase in a form footer reads as a warning message rather than a button. */
.modal-footer [data-bn-queue-archive],
.modal-footer [data-bn-task-archive] {
  --bs-btn-padding-x: 0.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  text-decoration: none;
}

.modal-footer [data-bn-queue-archive]:hover,
.modal-footer [data-bn-task-archive]:hover {
  background-color: var(--bn-fill-danger);
  border-radius: var(--bn-radius-sm);
}

.bn-tq-edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--bn-radius-sm);
  color: var(--bs-tertiary-color);
}

.bn-tq-edit:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-primary);
}

/* Revealed on hover where there is a pointer to reveal it with, and permanent
   where there is not. */
@media (hover: hover) {
  .bn-tq-act { visibility: hidden; }
  .bn-tq-row:hover .bn-tq-act,
  .bn-tq-row:focus-within .bn-tq-act { visibility: visible; }
}

/* Narrow lists drop the two cells that are context rather than the task —
   which record it hangs off, and how urgent it is. The title, the date and who
   has it are what the page is for.

   A CONTAINER QUERY, NOT A VIEWPORT ONE, and that is the lesson the chart rows
   taught the hard way: a viewport media query cannot see that the content area
   is already inset by a 264px sidebar, so it drops cells at a window width that
   has nothing to do with the width the row actually got. The group declares
   itself the container; the query asks it. Everything here is a `display: none`
   on a cell whose absence changes nothing else, so an engine without container
   queries simply keeps all five columns — which is the right fallback. */
.bn-tq-group { container-type: inline-size; container-name: bn-tq; }

@container bn-tq (max-width: 46rem) {
  .bn-tq-rec { display: none; }
}

@container bn-tq (max-width: 34rem) {
  .bn-tq-prio { display: none; }
  .bn-tq-due { flex-basis: 6rem; }
}

/* ---------------------------------------------------------------------------
 * Charts
 *
 * Every mark here is a div with a percentage width or height — no SVG except
 * the one path in the area chart, and no charting library. The reasoning is at
 * the top of views/partials/charts.ejs; the short version is that an SVG scaled
 * to its container scales its text with it, and these cards range from a third
 * of the page to the full width of it.
 *
 * The two spacers do the separating, not borders: a 2px gap in the surface
 * colour between touching segments, and a 2px surface ring on anything that
 * overlaps. A stroke around a mark would add ink that is not data.
 *
 * Bars are capped at 24px and the leftover band is left as air. Data ends are
 * rounded 4px and baselines stay square, so which end is the baseline is
 * readable without the axis.
 * ------------------------------------------------------------------------ */

.bn-viz-band-head {
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
  margin-bottom: 1rem;
}

.bn-viz-band-head h2 {
  font-size: 0.9375rem;
  margin: 0;
  color: var(--bs-emphasis-color);
}

.bn-viz-band-head p {
  margin: 0;
  font-size: 0.75rem;
  color: var(--bs-tertiary-color);
}

/* Twelve tracks, so a card can ask for a half, a third or the lot and the row
   still closes. Below 1100px everything becomes one column — the same
   breakpoint .bn-two-col uses, so the whole page reflows at one width. */
/* Cards in a row share a height — `stretch`, not the `start` .bn-two-col uses.
   These are read across, and a row of cards whose bottoms all land in different
   places reads as a broken grid rather than as cards of honest length. The
   flex column on .bn-viz-card keeps the CONTENT at the top of the taller ones,
   so nothing floats in the middle of its own card. */
.bn-viz-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--bn-stack-sm);
  align-items: stretch;
}

/* The table twin sits at the bottom of the card wherever the plot ends, so the
   disclosure rows line up across a stretched row instead of stepping. */
.bn-viz-card > .bn-viz-table { margin-top: auto; padding-top: 1rem; }

.bn-viz-span-4 { grid-column: span 4; }
.bn-viz-span-6 { grid-column: span 6; }
.bn-viz-span-8 { grid-column: span 8; }
.bn-viz-span-12 { grid-column: span 12; }

@media (max-width: 1100px) {
  .bn-viz-grid { grid-template-columns: minmax(0, 1fr); }
  .bn-viz-grid > * { grid-column: 1 / -1; }
}

.bn-viz-card { display: flex; flex-direction: column; }

.bn-viz-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
}

.bn-viz-head .bn-i-sm { color: var(--bs-tertiary-color); }

.bn-viz-sub {
  margin: 0.25rem 0 0;
  font-size: 0.75rem;
  color: var(--bs-tertiary-color);
}

/* The one number a card leads with. Proportional figures, not tabular — equal
   width digits make a large standalone number look loose. */
.bn-viz-lede {
  margin-top: 0.875rem;
  font-size: 1.375rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--bs-emphasis-color);
}

.bn-viz-lede small {
  display: block;
  margin-top: 0.125rem;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--bs-tertiary-color);
}

.bn-viz-plot { margin-top: 1.125rem; }

/* WHY THE CHART IS STILL RENDERED WHEN IT IS EMPTY. A note under an empty axis
   says which KIND of empty it is — nothing happened, or nothing was recorded —
   which is the rule My day's work lists already follow. Hiding the chart would
   throw that distinction away, and an axis of twelve empty months is itself the
   most honest thing the page can draw. */
.bn-viz-note {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin: 1rem 0 0;
  padding-top: 0.875rem;
  border-top: 1px solid var(--bs-border-color);
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-viz-note .bn-i { flex: 0 0 auto; color: var(--bs-tertiary-color); margin-top: 0.0625rem; }
.bn-viz-note a { color: var(--bs-primary); }

/* --- Legend -------------------------------------------------------------- */

/* Present whenever there are two or more series and absent at one, where the
   card heading already names what is drawn. Text wears text tokens; the
   coloured swatch beside it carries the identity. */
.bn-viz-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin: 0.875rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.bn-viz-legend li { display: flex; align-items: center; gap: 0.4375rem; }

.bn-viz-key {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 0.1875rem;
  flex: 0 0 auto;
}

.bn-viz-key-line {
  width: 0.875rem;
  height: 2px;
  border-radius: 1px;
  flex: 0 0 auto;
}

/* --- Horizontal bars ----------------------------------------------------- */

.bn-viz-rows { display: flex; flex-direction: column; gap: 0.625rem; }

/* THE LABEL COLUMN HAS TO BE ABLE TO SHRINK, and a viewport media query cannot
   be what makes it. These cards are a third of a grid inside a page inset by a
   264px sidebar, so how much room a bar has is a function of the CARD's width,
   not the window's — at a 600px viewport the card is ~224px wide while the
   window is still far above any sensible breakpoint. Written as a fixed 6rem
   minimum plus a 4.5rem minimum for the value, the two ends claimed 192px of
   224px and the track resolved to ZERO: rows of labels with no bar between
   them, only under the narrow layout, which is exactly the bug a media query
   would have been written to fix and could not.

   So the ends are `minmax(0, …)` and the track keeps the only floor. The label
   already truncates with an ellipsis, so it degrades by getting shorter rather
   than by pushing the data off the row. */
.bn-viz-row,
.bn-viz-drow {
  display: grid;
  grid-template-columns: minmax(0, 8rem) minmax(2.5rem, 1fr) minmax(0, auto);
  align-items: center;
  gap: 0.75rem;
}

@media (max-width: 640px) {
  .bn-viz-row,
  .bn-viz-drow { gap: 0.5rem; }
}

.bn-viz-row-label {
  min-width: 0;
  font-size: 0.75rem;
  color: var(--bs-body-color);
}

.bn-viz-row-label > span,
.bn-viz-row-label > a {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bn-viz-row-label > a { color: inherit; text-decoration: none; }
.bn-viz-row-label > a:hover { color: var(--bs-primary); }

.bn-viz-row-label em {
  display: block;
  font-style: normal;
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
}

.bn-viz-track {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 0.875rem;
  border-radius: 0.25rem;
}

.bn-viz-rows-track .bn-viz-track { background-color: var(--bn-viz-track); }

.bn-viz-seg {
  display: block;
  height: 100%;
  min-width: 2px;
  border-radius: 0;
  transition: filter 0.12s ease;
}

.bn-viz-seg:last-child { border-radius: 0 0.25rem 0.25rem 0; }

/* A stage holding nothing still draws a row. The stub is the baseline, not a
   value — it is the surface's own axis colour rather than a series colour, so
   it cannot be misread as a very small bar. */
.bn-viz-seg-empty {
  width: 0.375rem;
  background-color: var(--bn-viz-axis);
  border-radius: 0.25rem;
}

.bn-viz-row-value {
  font-size: 0.75rem;
  font-weight: 600;
  text-align: right;
  color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums;
}

.bn-viz-row-value-zero { color: var(--bs-tertiary-color); font-weight: 400; }

/* --- Columns ------------------------------------------------------------- */

/* THE BASELINE IS ONE RULE ACROSS THE CHART, not a border under each column.
   Drawn per column it inherits the 0.375rem gap between them and comes out as
   a dashed line — which the anti-pattern list rules out for exactly the reason
   it looks wrong here: a broken rule reads as a threshold or a projection when
   it is only the axis. So the columns draw no border and the container draws
   one continuous hairline at the height the bars grow from. */
.bn-viz-cols,
.bn-viz-dcols {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0.375rem;
}

.bn-viz-cols::after,
.bn-viz-dcols::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background-color: var(--bn-viz-axis);
  pointer-events: none;
}

.bn-viz-cols::after { top: 6rem; }
.bn-viz-dcols::after { top: 3rem; }

.bn-viz-col { flex: 1 1 0; min-width: 0; text-align: center; }

.bn-viz-col-plot {
  height: 6rem;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  gap: 0.25rem;
}

.bn-viz-col-bar {
  display: block;
  width: 100%;
  max-width: 1.5rem;
  min-height: 2px;
  border-radius: 0.25rem 0.25rem 0 0;
  transition: filter 0.12s ease;
}

.bn-viz-col-cap {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums;
}

.bn-viz-col-cap-zero { color: var(--bs-tertiary-color); font-weight: 400; }

.bn-viz-col-label,
.bn-viz-dcol-label,
.bn-viz-axis span {
  margin-top: 0.4375rem;
  font-size: 0.6875rem;
  line-height: 1.3;
  color: var(--bs-tertiary-color);
  overflow-wrap: anywhere;
}

/* --- Diverging columns --------------------------------------------------- */

.bn-viz-dcol { flex: 1 1 0; min-width: 0; text-align: center; }

.bn-viz-dcol-up,
.bn-viz-dcol-down {
  height: 3rem;
  display: flex;
  justify-content: center;
}

.bn-viz-dcol-up { align-items: flex-end; }
.bn-viz-dcol-down { align-items: flex-start; }

.bn-viz-dcol-up > i,
.bn-viz-dcol-down > i {
  display: block;
  width: 100%;
  max-width: 1.5rem;
  transition: filter 0.12s ease;
}

/* The 2px surface gap, split either side of the zero line — the two arms are
   touching marks and the same rule applies to them as to the segments of a
   stacked bar. Without it a month with wins and losses reads as one bar in two
   colours crossing the axis, rather than as two bars meeting at it. */
.bn-viz-dcol-up > i { border-radius: 0.25rem 0.25rem 0 0; margin-bottom: 1px; }
.bn-viz-dcol-down > i { border-radius: 0 0 0.25rem 0.25rem; margin-top: 1px; }

/* --- Diverging bars ------------------------------------------------------ */

.bn-viz-drows { display: flex; flex-direction: column; gap: 0.625rem; }

.bn-viz-dtrack {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  height: 0.875rem;
  position: relative;
  background-color: var(--bn-viz-track);
  border-radius: 0.25rem;
}

/* The midpoint is a neutral rule, never a third hue — a colour here would read
   as a value rather than as zero. */
.bn-viz-dtrack::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -0.125rem;
  bottom: -0.125rem;
  width: 1px;
  background-color: var(--bn-viz-axis);
}

.bn-viz-dbar {
  display: block;
  height: 100%;
  transition: filter 0.12s ease;
}

.bn-viz-dbar-neg { justify-self: end; border-radius: 0.25rem 0 0 0.25rem; }
.bn-viz-dbar-pos { justify-self: start; border-radius: 0 0.25rem 0.25rem 0; }

/* Exactly on the line is a reading, not a missing bar. The 2px ring is the
   surface colour so the dot stays legible where it sits over the midpoint
   rule. */
.bn-viz-dzero {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.5rem;
  height: 0.5rem;
  margin: -0.25rem 0 0 -0.25rem;
  border-radius: 50%;
  background-color: var(--bn-viz-muted);
  box-shadow: 0 0 0 2px var(--bn-surface);
}

/* --- Area ---------------------------------------------------------------- */

.bn-viz-area {
  position: relative;
  height: 5.5rem;
  border-bottom: 1px solid var(--bn-viz-axis);
}

/* The only SVG in the app's charts, and it holds the path and nothing else.
   preserveAspectRatio="none" is what lets it fill any column width; it is also
   why nothing round or lettered may go inside it. The stroke survives that
   stretch only because of vector-effect, set in the markup. */
.bn-viz-area-svg {
  display: block;
  width: 100%;
  height: 100%;
}

.bn-viz-area-fill { opacity: 0.1; }
.bn-viz-area-line { stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }

/* One full-height target per period, so the pointer aims at a week rather than
   at a 2px line. This is the crosshair's job done with layout. */
.bn-viz-area-bands {
  position: absolute;
  inset: 0;
  display: flex;
}

.bn-viz-band { flex: 1 1 0; cursor: default; }

.bn-viz-band:hover,
.bn-viz-band:focus-visible {
  background-color: var(--bn-wash-strong);
  outline: none;
}

.bn-viz-axis { display: flex; }
.bn-viz-axis span { flex: 1 1 0; min-width: 0; text-align: center; }

/* --- Hover and focus ----------------------------------------------------- */

[data-bn-viz-tip] { outline: none; }

[data-bn-viz-tip]:hover .bn-viz-seg,
[data-bn-viz-tip]:hover .bn-viz-col-bar,
[data-bn-viz-tip]:hover .bn-viz-dbar,
[data-bn-viz-tip]:hover .bn-viz-dcol-up > i,
[data-bn-viz-tip]:hover .bn-viz-dcol-down > i { filter: brightness(1.12); }

[data-bn-viz-tip]:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
  border-radius: 0.25rem;
}

/* --- Tooltip ------------------------------------------------------------- */

.bn-viz-tip {
  position: fixed;
  z-index: 1080;
  max-width: 15rem;
  padding: 0.5rem 0.625rem;
  border-radius: var(--bn-radius-sm);
  background-color: var(--bn-invert-bg);
  color: var(--bn-invert-fg);
  font-size: 0.75rem;
  line-height: 1.45;
  pointer-events: none;
  box-shadow: var(--bn-lift-lg);
}

/* The value leads and the label follows — the legend's hierarchy inverted,
   because by the time someone is hovering they have the series and want the
   number. */
.bn-viz-tip b { font-weight: 600; }
.bn-viz-tip span { display: block; color: var(--bn-invert-fg-dim); }

/* --- Table twin ---------------------------------------------------------- */

/* Every chart has one. It is what makes a sub-3:1 mark legal, it is the
   keyboard and screen-reader path to the same numbers, and it is the only way
   to copy a value out of a chart made of divs. */
.bn-viz-table { margin-top: 1rem; }

.bn-viz-table > summary {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}

.bn-viz-table > summary::-webkit-details-marker { display: none; }

.bn-viz-table > summary::before {
  content: "";
  width: 0.375rem;
  height: 0.375rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}

.bn-viz-table[open] > summary::before { transform: rotate(45deg); }
.bn-viz-table > summary:hover { color: var(--bs-primary); }

/* Wide content scrolls inside its own box; the page never scrolls sideways. */
.bn-viz-table-scroll { overflow-x: auto; margin-top: 0.625rem; }

.bn-viz-table table {
  width: 100%;
  font-size: 0.75rem;
  border-collapse: collapse;
}

.bn-viz-table th,
.bn-viz-table td {
  padding: 0.375rem 0.5rem;
  border-top: 1px solid var(--bs-border-color);
  text-align: left;
  font-weight: 400;
  white-space: nowrap;
}

.bn-viz-table thead th {
  border-top: 0;
  font-size: 0.6875rem;
  color: var(--bs-tertiary-color);
}

.bn-viz-table tbody th { color: var(--bs-body-color); }
.bn-viz-table .bn-num-cell { text-align: right; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .bn-viz-seg,
  .bn-viz-col-bar,
  .bn-viz-dbar,
  .bn-viz-dcol-up > i,
  .bn-viz-dcol-down > i,
  .bn-viz-table > summary::before { transition: none; }
}

/* ===========================================================================
 * Environment banner
 *
 * Emitted by views/partials/env-banner.ejs, and only when BN_ENV_LABEL is set —
 * which only the sandbox stack does. The real app never renders this markup.
 *
 * FIXED, and `pointer-events: none` on the strip, because it is included from
 * BOTH layouts and the auth layout has no shell to lay out against. Nothing
 * here may take part in flow: a banner that shifted the sidebar down by its own
 * height would make the sandbox stop being a rehearsal of the real thing.
 *
 * The fill is `--bs-warning`, so it follows the Theme tab. The INK is a literal
 * and deliberately not a token, which is the `--bs-dark` trap from CLAUDE.md in
 * a new place: a theme colour is not a mode, so `--bs-warning` is the same amber
 * in dark mode, and any ink that flips is wrong half the time. `--bn-invert-fg`
 * is #fff in light mode — white on #F79009 is 2.1:1 and fails. This ink is
 * 7.6:1 against it in both modes, which is why there is no dark block below.
 * ======================================================================== */

.bn-env-banner {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 1090;                    /* over the modal backdrop at 1080 */
  height: 3px;
  background: var(--bs-warning);
  pointer-events: none;
  display: flex;
  justify-content: center;
}

.bn-env-banner-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-top: 3px;
  padding: 0.125rem 0.625rem 0.1875rem;
  border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
  background: var(--bs-warning);
  color: #18181b;                   /* see the note above — must NOT flip */
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* The one place an uppercase micro-label is right: this is not a heading, it is
   a warning light, and it should read as a different KIND of thing from every
   real label on the page. */
.bn-env-banner-pill > span { text-transform: uppercase; }

@media (max-width: 575.98px) {
  .bn-env-banner-pill > span { display: none; }
  .bn-env-banner-pill { padding: 0.25rem; }
}

/* ===========================================================================
 * Dark mode
 *
 * THIS IS THE ONE PLACE IN THIS FILE THAT MAY DECLARE A `--bs-*` VARIABLE, and
 * the reason is in CLAUDE.md → Theme: Wappler's Theme tab silently discards
 * every `$*-dark` Sass variable it is given, so the compiler emits Bootstrap's
 * STOCK dark palette — a blue-grey #212529 — no matter what the theme says.
 * There is no supported hook to change that, so dark mode has to be written by
 * hand, and CSS is where it has to live.
 *
 * That is safe in a way a `:root` override would not be. Everything below is
 * scoped to `[data-bs-theme="dark"]`, so light mode is still entirely the Sass
 * build's and the Theme tab keeps working. Nothing here is a light-mode value.
 *
 * The attribute goes on <html> by views/partials/theme.ejs, which also carries
 * the switch's handler. `[data-bs-theme]` is Bootstrap's own mechanism rather
 * than a class of ours, which is what makes the form-select chevron, the
 * form-switch knob and the .btn-close filter flip themselves — Bootstrap ships
 * dark rules for all three and they cost nothing here.
 *
 * WHY BOOTSTRAP'S OWN DARK VALUES ARE ALL REPLACED. Its dark palette inverts
 * the relationship this app is built on: it makes `--bs-secondary-bg` LIGHTER
 * than `--bs-body-bg`, and since the canvas is secondary-bg and every card is
 * body-bg, taking it as-is would have punched every card into the page as a
 * hole. The surfaces below keep the light-mode order — a card stands above the
 * canvas — which is the whole reason `--bn-canvas` was split out of
 * `--bs-secondary-bg` at the top of this file.
 * ======================================================================== */

[data-bs-theme="dark"] {
  /* --- Surfaces ---------------------------------------------------------
     Five steps, darkest first: chrome (sidebar/topbar), the canvas the page
     sits on, the raised surface every card/tile/modal is, and two recesses
     inside it.

     Chrome, canvas and surface are picked hexes rather than derived — #1D1D1F,
     #202021, #29292A — and sit close together on purpose, almost flat. The
     recesses are NOT picked; they are the old proportional step up from the
     surface (secondary +11/+11/+13, tertiary +18/+18/+21 in raw RGB), carried
     over so a search field, a table header and a hover state stay readable as
     "separated from the surface" against the new, lighter card colour rather
     than landing darker than the card they recess into — which is the
     opposite of what a recess means. Revisit these two when picked colours
     for them exist.

     The order that matters is kept: canvas < surface, in both modes; chrome
     is new and sits apart from that pair rather than replacing either end of
     it. */
  --bn-chrome: #1d1d1f;
  --bn-canvas: #181818;
  /* The same mix, further along it: 5% of primary is invisible against a dark
     card, and the point of the token is that this end of it can move on its own.
     Measured against #1D1D1D it lands near rgb(37,38,54) — a step up, and the
     one direction "lighter" can actually mean here. */
  --bn-surface-hover: color-mix(in srgb, var(--bs-primary) 3%, var(--bn-surface));

  /* The nav's two steps, raised for the same reason and by the same amount of
     reasoning: 6% and 12% of an indigo primary are clearly visible mixed into
     white and very nearly invisible mixed into #1D1D1F. Measured on the dark
     chrome, 12% and 22% land the pair a comparable distance apart and a
     comparable distance from the rail — which is what the tokens are for, so
     that this is one edit rather than one per call site. The ink they carry
     moves on its own: --bn-on-primary hands over to Bootstrap's dark
     `*-text-emphasis` a few lines below. */
  --bn-nav-hover: color-mix(in srgb, var(--bs-primary) 1%, var(--bn-chrome));
  --bn-nav-active: color-mix(in srgb, var(--bs-primary) 5%, var(--bn-chrome));

  --bs-body-bg: #1D1D1D;
  --bs-body-bg-rgb: 24, 24, 24;
  --bs-secondary-bg: #343437;
  --bs-secondary-bg-rgb: 29, 29, 29;
  --bs-tertiary-bg: #272727;
  --bs-tertiary-bg-rgb: 39, 39, 39;

  /* --- Ink ---------------------------------------------------------------
     Picked hexes, not derived: body #BCBBBE, emphasis #F7F7F9, tertiary
     #908E94 — the same three roles the sidebar nav uses (idle text, selected
     text, section-label text) applied as the app's general ink ramp, so
     every place that already reads body/emphasis/tertiary-color picks them
     up uniformly rather than the sidebar alone. Measured on the surface:
     7.61, 13.58 and 4.49:1. Secondary is untouched at #a1a1aa (6.91:1) — it
     still falls between the new body and tertiary tones, so the ramp's own
     ordering (emphasis > body > secondary > tertiary) still holds.

     The `-rgb` twin of each has to be kept in step BY HAND here; in light mode
     Sass emits both from one declaration. `.bg-body-*` reads the rgb form
     while `.text-body-*` reads the hex, so dropping one of a pair gives you a
     background that ignores the mode and looks like a caching fault. */
  --bs-body-color: #bcbbbe;
  --bs-body-color-rgb: 188, 187, 190;
  --bs-emphasis-color: #f7f7f9;
  --bs-emphasis-color-rgb: 247, 247, 249;
  --bs-secondary-color: #a1a1aa;
  --bs-secondary-color-rgb: 161, 161, 170;
  --bs-tertiary-color: #908e94;
  --bs-tertiary-color-rgb: 144, 142, 148;

  /* #2f2f37 measured 1.09:1 against the #29292a surface — a hairline that is
     mathematically present and visually absent, which is what made every
     input, filter-bar trigger and board-column edge disappear in this mode:
     .form-control and .form-select both read this token straight off
     Bootstrap's compiled CSS with no override of their own (see the note
     above .form-control), so one washed-out value here washed out every
     bordered control in the app at once. #3d3d47 measures 1.35:1 on the same
     surface — still a hairline, not a stroke, but one a control can actually
     be told apart from its own background by. */
  --bs-border-color: #242424;
  --bs-border-color-translucent: rgba(255, 255, 255, 0.12);

  /* --- Lift --------------------------------------------------------------
     The light shadows are 4%-opacity near-black, which over a dark surface is
     invisible. Darker and longer-throw, because on a dark canvas a shadow is
     the only thing separating a modal from the page behind it. */
  --bn-lift: 0 1px 2px rgba(0, 0, 0, 0.4);
  --bn-lift-lg: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 28px -10px rgba(0, 0, 0, 0.7);
  --bn-border-hover: #45454f;

  /* The inverted pair, inverted. */
  --bn-invert-bg: #e4e4e7;
  --bn-invert-fg: #18181b;
  --bn-invert-fg-dim: rgba(24, 24, 27, 0.62);

  /* `--bs-dark` is #18181B in both modes, which lands within a shade of the
     surface — the brand square would vanish. Primary is the mark's other
     brand colour and holds white at 4.47:1. */
  --bn-mark-bg: var(--bs-primary);

  /* --- Status ------------------------------------------------------------
     A 12% wash disappears against a dark surface, so the same mix runs at 20%
     and the borders at 45%. The ink hands over to Bootstrap's dark
     `*-text-emphasis`, which Sass derives from the themed colours — so these
     follow the Theme tab without being restated. Measured on their own tints:
     6.14, 7.04, 7.38, 6.22 and 6.74:1, against 4.25–6.81 in light mode. */
  --bn-fill-primary: color-mix(in srgb, var(--bs-primary) 20%, transparent);
  --bn-fill-success: color-mix(in srgb, var(--bs-success) 20%, transparent);
  --bn-fill-warning: color-mix(in srgb, var(--bs-warning) 20%, transparent);
  --bn-fill-danger:  color-mix(in srgb, var(--bs-danger)  20%, transparent);
  --bn-fill-info:    color-mix(in srgb, var(--bs-info)    20%, transparent);

  --bn-edge-success: color-mix(in srgb, var(--bs-success) 45%, transparent);
  --bn-edge-warning: color-mix(in srgb, var(--bs-warning) 45%, transparent);
  --bn-edge-danger:  color-mix(in srgb, var(--bs-danger)  45%, transparent);

  --bn-on-primary: var(--bs-primary-text-emphasis);
  --bn-on-success: var(--bs-success-text-emphasis);
  --bn-on-warning: var(--bs-warning-text-emphasis);
  --bn-on-danger:  var(--bs-danger-text-emphasis);
  --bn-on-info:    var(--bs-info-text-emphasis);

  --bn-wash: color-mix(in srgb, var(--bs-primary) 10%, transparent);
  --bn-wash-strong: color-mix(in srgb, var(--bs-primary) 16%, transparent);

  /* --- Identity hues -----------------------------------------------------
     Same six hues, stepped to mirror the light set's own lightness spread so
     they stay as separable from each other here as they are there. Every
     ink-on-tint pairing measures 5.9:1 or better; the light set's tightest is
     4.5:1. See the note in the token block: these are avatar and timeline
     marks, never the sole carrier of anything. */
  --bn-hue-indigo:  #b2c0ff;
  --bn-hue-emerald: #86cdae;
  --bn-hue-amber:   #e89568;
  --bn-hue-fuchsia: #e992f1;
  --bn-hue-sky:     #86c5f7;
  --bn-hue-rose:    #ff8b94;

  --bn-tint-indigo:  #252953;
  --bn-tint-emerald: #133528;
  --bn-tint-amber:   #47220c;
  --bn-tint-fuchsia: #401f43;
  --bn-tint-sky:     #123146;
  --bn-tint-rose:    #4d1b20;

  /* --- Data visualisation ------------------------------------------------
     THESE WERE VALIDATED THE SAME WAY THE LIGHT ONES WERE, against #18181c —
     the card surface a chart actually sits on in this mode — and not derived
     from the light values by any rule. Re-run the check before changing a hex
     here, exactly as the light block says.

     The ordinal ramp RUNS THE OTHER WAY, and that is the point rather than an
     oversight. seq-1 is the smallest deal band and seq-6 the largest, and the
     encoding is "more value, more ink" — which on white means darker and on a
     dark surface means lighter. Reversing it would leave the largest band as
     the step closest to the surface, i.e. the faintest. Six steps at ΔL 0.07,
     one hue (spread 1°), and the near-surface end still clears the surface at
     3.58:1 where the floor is 2:1.

     The diverging pair measures ΔE 25.9 under protanopia and 29.7 under normal
     vision, clear of the 8 and 15 floors. `--bn-viz-muted` fails the chroma
     floor here just as it does in light mode, and for the same stated reason:
     it is meant to read as grey. It holds ΔE 19.7 from the accent it is drawn
     beside — light mode's is 20.8 — and 3.21:1 against the surface. */
  --bn-viz-seq-1: #5d61e0;
  --bn-viz-seq-2: #737ce9;
  --bn-viz-seq-3: #8a95f1;
  --bn-viz-seq-4: #a3aef8;
  --bn-viz-seq-5: #bdc7fe;
  --bn-viz-seq-6: #d8dfff;

  --bn-viz-accent: #6e75f2;
  --bn-viz-pos: #6e75f2;
  --bn-viz-neg: #df5548;
  --bn-viz-muted: #686871;

  --bn-viz-grid: #26262c;
  --bn-viz-axis: #3f3f46;
  --bn-viz-track: #232329;
}

/* The area chart's fill is a flat opacity rather than a colour, so it does not
   follow a token. A 10% wash of a light indigo over a dark surface is close to
   nothing; the line stays 2px and does the reading either way. */
[data-bs-theme="dark"] .bn-viz-area-fill { opacity: 0.18; }

/* `.btn-dark` IS THIS APP'S PRIMARY ACTION BUTTON — Save, Add company, Add
   deal, New, 34 of them — and Bootstrap does not flip it, because `dark` is a
   fixed theme colour rather than a mode. `--bs-dark` is #18181B, so on a
   #18181c surface the fill vanishes and the button becomes bare white text
   with no edge: still clickable, no longer a button. It is the same problem
   `--bn-mark-bg` solves for the brand square.

   The inverted pair is what it should be in both modes — a near-black button
   on a white page, a near-white one on a dark page — so it takes the same two
   tokens. Written through Bootstrap's own per-component `--bs-btn-*` API,
   which is a documented surface and not a theme override. */
[data-bs-theme="dark"] .btn-dark {
  --bs-btn-color: var(--bn-invert-fg);
  --bs-btn-bg: var(--bn-invert-bg);
  --bs-btn-border-color: var(--bn-invert-bg);
  --bs-btn-hover-color: var(--bn-invert-fg);
  --bs-btn-hover-bg: #f4f4f5;
  --bs-btn-hover-border-color: #f4f4f5;
  --bs-btn-active-color: var(--bn-invert-fg);
  --bs-btn-active-bg: #fafafa;
  --bs-btn-active-border-color: #fafafa;
  --bs-btn-disabled-color: var(--bn-invert-fg);
  --bs-btn-disabled-bg: var(--bn-invert-bg);
  --bs-btn-disabled-border-color: var(--bn-invert-bg);
  --bs-btn-focus-shadow-rgb: 228, 228, 231;
}

/* The secondary action beside it, 31 uses. The border survives the mode — it
   is `--bs-secondary`, mid-grey, visible on either surface — but the LABEL in
   that same grey reads at 3.66:1 on the dark surface, which is a border's
   contrast, not a word's. Only the resting label is lifted; the hover state
   fills with secondary and puts white on it in both modes. */
[data-bs-theme="dark"] .btn-outline-secondary {
  --bs-btn-color: var(--bs-body-color);
}

/* The filter bar's "this one is filtering" wash. It is 8% of the primary over
   the card in light mode, which on the dark surface lands inside a shade of
   the surface itself — the same trap as every other tint in this block, and
   the same fix: one number, not a second colour. The border is what carries
   the state at small sizes, so it moves further than the fill. */
[data-bs-theme="dark"] .bn-fb-menu.is-on > .bn-fb-trigger {
  border-color: color-mix(in srgb, var(--bs-primary) 60%, var(--bs-border-color));
  background-color: color-mix(in srgb, var(--bs-primary) 18%, var(--bn-surface));
}

/* The queue's In-progress row. 6% of the info colour over a white card is a
   visible tint; over #1D1D1D it lands inside a shade of the surface itself and
   the row reads as ordinary — the same trap every tint in this block falls
   into, and the same one-number fix. The inset accent already carries the state
   at full strength, so the fill only has to be noticed, not read. */
[data-bs-theme="dark"] .bn-tq-row-working {
  background-color: color-mix(in srgb, var(--bs-info) 14%, var(--bn-surface));
}

[data-bs-theme="dark"] .bn-tq-row-working:hover {
  background-color: color-mix(in srgb, var(--bs-info) 20%, var(--bn-surface));
}

/* A done row is dimmed by opacity in light mode, which on a dark surface takes
   it toward the canvas rather than away from it — the row stops looking
   finished and starts looking like a hole. The line through the title and the
   filled circle are what say "done" here. */
[data-bs-theme="dark"] .bn-tq-row-done { opacity: 0.85; }

/* ---------------------------------------------------------------------------
 * The switch
 *
 * A real switch, not a checkbox: `role="switch"` on a <button>, so it announces
 * its own on/off state without a form control — and without giving App Connect
 * a <form> to upgrade and hijack (CLAUDE.md → Forms inside App Connect).
 *
 * BOTH ICONS ARE ALWAYS IN THE DOM and CSS hides one. Swapping them in JS would
 * mean writing a new <i data-lucide> and waiting for the fork's MutationObserver
 * to reach it — a flicker on every toggle — and the sibling rule that governs
 * every other icon in this app applies here too: nothing assigns to this
 * button's textContent, so the icons are safe where they are.
 *
 * Each icon is wrapped in a span rather than being hidden directly. Lucide
 * REPLACES the <i> with an <svg> it builds itself, so a rule written against
 * the placeholder stops matching the moment the library runs. The span is not
 * touched by any of that.
 * ------------------------------------------------------------------------ */

/* THE SWITCH IS A BARE ICON. It was a 52px pill with a lifted knob sliding
   across a recessed track, and it stood in a bar whose other controls are flat
   icons with no fill at all — a piece of switch hardware next to two pieces of
   chrome. It takes .bn-icon-btn now and has almost nothing of its own left.

   The state is still readable and by the same means it always was: the glyph is
   the mode you are IN, and aria-checked plus the label say what pressing it
   would do. What went is the track, the knob and the travel — a control that
   has to animate to report a state that one glyph already reports.

   The dark-mode warmth is kept, because it is the one thing the fill used to
   carry that the glyph could not: a moon that reads as lit rather than as
   another grey icon in a row of them. */
/* The scrim, heavier in dark. 55% black over a white page is a mid grey and
   an obvious change of state; the same 55% over #181818 is barely a change at
   all, because there is very little light left in the page to take away. */
[data-bs-theme="dark"] .bn-search-backdrop { background-color: rgba(0, 0, 0, 0.72); }

[data-bs-theme="dark"] .bn-theme-switch { color: var(--bs-warning); }

/* Which symbol is shown: the mode you are IN, not the one you would get.
   A switch reports state; its label says what it will do.
   Order matters — the base rule and the light-mode hide have equal
   specificity, so the base has to come first. */
.bn-theme-switch [data-bn-theme-icon] { display: grid; }
.bn-theme-switch [data-bn-theme-icon="dark"] { display: none; }
[data-bs-theme="dark"] .bn-theme-switch [data-bn-theme-icon="light"] { display: none; }
[data-bs-theme="dark"] .bn-theme-switch [data-bn-theme-icon="dark"] { display: grid; }
