/*
 * Fund Fridge design tokens — the design system for the WHOLE application.
 *
 * These began as the ledger's, because that is where the redesign started, and
 * they are now the product's. Every screen wears them: the money screens, the
 * office screens, the library, the Django admin, and anything added next. There
 * is no second look and no per-app palette; a screen that does not read like
 * this one is a screen that has not been done yet, not a screen with its own
 * style.
 *
 * Loaded before every other stylesheet — by `templates/base.html` for the app
 * and by `templates/admin/base_site.html` for the admin — and the only place a
 * colour, a radius, a type size or a shadow is allowed to be invented. Six stylesheets grew in
 * parallel with none of this, and it showed: five different small-text sizes,
 * two radii, and a palette that was half Bootstrap grey and half hand-picked
 * hex. Everything read correct and nothing read considered.
 *
 * Two ideas carry the look.
 *
 * **Cards go darker than the page, not lighter.** The reflex is to lift a card
 * off the background with a paler fill, which in a dark interface just makes
 * everything grey. Sinking the important surface instead gives the page real
 * depth and lets the numbers on it sit in their own pool of quiet.
 *
 * **Colour is information, not decoration.** Lime is the one accent: the active
 * screen, the primary action, the figure the page exists to show. Beyond it
 * there is exactly one semantic set, and rose only ever means money going the
 * wrong way. In a finance tool that is not an extra palette, it is the data.
 */

@import url("https://cdn.jsdelivr.net/npm/@fontsource-variable/geist@5/index.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource-variable/geist-mono@5/index.css");

:root {
    /* Surfaces, darkest last. The page sits above its own content. */
    --ff-bg: #212426;
    --ff-surface: #191c1e;
    --ff-deep: #0d0f11;
    --ff-rail: #131517;
    --ff-line: #2c3033;
    --ff-line-soft: #23272a;

    --ff-text: #e9ecec;
    --ff-dim: #9aa1a5;
    --ff-faint: #6b7378;

    /* The one accent. */
    --ff-accent: #c9f75a;
    --ff-accent-soft: rgba(201, 247, 90, .13);
    --ff-accent-line: rgba(201, 247, 90, .32);
    --ff-on-accent: #141a04;

    /* The semantic set. Money up, money down, needs a look, worth knowing. */
    --ff-up: #a8e05f;
    --ff-down: #ff6f6f;
    --ff-warn: #f8b95c;
    --ff-info: #8f83ec;
    --ff-up-soft: rgba(168, 224, 95, .14);
    --ff-down-soft: rgba(255, 111, 111, .14);
    --ff-warn-soft: rgba(248, 185, 92, .14);
    --ff-info-soft: rgba(143, 131, 236, .16);
    /* A stronger pour for the tile wash, which sits over a larger area and
       reads as grime rather than colour at the chip's strength. */
    --ff-up-wash: rgba(168, 224, 95, .3);
    --ff-down-wash: rgba(255, 111, 111, .3);
    --ff-warn-wash: rgba(248, 185, 92, .3);
    --ff-info-wash: rgba(143, 131, 236, .34);
    --ff-accent-wash: rgba(201, 247, 90, .28);

    /*
     * A sequential ramp, and the only one. Not an accent and not a semantic
     * colour: it carries *how much*, over hundreds of cells, where neither
     * "act on this" nor "up/down/warn" is what is being said. The DDirt sync
     * strip is the case that needed it — two hundred day cells were mixed
     * towards the lime, which is the single largest spend of the accent
     * anywhere and left it meaning nothing on that screen.
     *
     * Teal, not green: green is `--ff-up` and sits next to the accent, and on
     * that strip the neighbouring cell is often `--ff-down` red. Teal against
     * red survives deuteranopia; a lime-green against red does not.
     *
     * If a second ramp ever seems necessary, it is not. Ramps stop reading as
     * quantity the moment there are two of them on one page.
     */
    --ff-data-cool: #2f7f8c;
    --ff-data-cool-deep: #63d8e6;

    /* One radius scale. Cards, controls, pills. Nothing else. */
    --ff-r-card: 16px;
    --ff-r-control: 10px;
    --ff-r-pill: 999px;

    /* One type scale. */
    --ff-t-micro: .6875rem;
    --ff-t-small: .8125rem;
    --ff-t-body: .9375rem;
    --ff-t-lead: 1.125rem;
    --ff-t-figure: 2.75rem;
    --ff-t-hero: 4rem;

    --ff-sans: "Geist Variable", "Geist", system-ui, -apple-system, "Segoe UI", sans-serif;
    --ff-mono: "Geist Mono Variable", "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;

    /* Shadows are tinted to the ground, never pure black on a dark field. */
    --ff-shadow: 0 1px 2px rgba(6, 8, 9, .5), 0 8px 24px -12px rgba(6, 8, 9, .8);
    --ff-ease: cubic-bezier(.16, 1, .3, 1);
}

/*
 * Bootstrap's own dark theme, re-pointed at the tokens. Doing it here rather
 * than overriding component by component means every `.table`, `.alert`,
 * `.btn` and `.form-select` already in the templates inherits the palette
 * without a single markup change.
 */
[data-bs-theme="dark"] {
    --bs-body-bg: var(--ff-bg);
    --bs-body-color: var(--ff-text);
    --bs-body-font-family: var(--ff-sans);
    --bs-body-font-size: var(--ff-t-body);
    --bs-emphasis-color: #fff;
    --bs-secondary-color: var(--ff-dim);
    --bs-tertiary-color: var(--ff-faint);
    --bs-secondary-bg: var(--ff-surface);
    --bs-tertiary-bg: var(--ff-line-soft);
    --bs-border-color: var(--ff-line);
    --bs-border-radius: var(--ff-r-control);
    --bs-border-radius-sm: 8px;
    --bs-border-radius-lg: var(--ff-r-card);

    --bs-primary: var(--ff-accent);
    --bs-primary-rgb: 201, 247, 90;
    --bs-link-color: var(--ff-accent);
    --bs-link-hover-color: #d8ff7d;
    /* Bootstrap composes link colour from the RGB triple, not from the named
       variable above, so without these two every inline link stayed the stock
       blue no matter what the theme said. */
    --bs-link-color-rgb: 201, 247, 90;
    --bs-link-hover-color-rgb: 216, 255, 125;

    --bs-success-text-emphasis: var(--ff-up);
    --bs-danger-text-emphasis: var(--ff-down);
    --bs-warning-text-emphasis: var(--ff-warn);
    --bs-info-text-emphasis: var(--ff-info);

    --bs-success-bg-subtle: var(--ff-up-soft);
    --bs-danger-bg-subtle: var(--ff-down-soft);
    --bs-warning-bg-subtle: var(--ff-warn-soft);
    --bs-info-bg-subtle: var(--ff-info-soft);
    --bs-success-border-subtle: rgba(168, 224, 95, .28);
    --bs-danger-border-subtle: rgba(255, 111, 111, .28);
    --bs-warning-border-subtle: rgba(248, 185, 92, .28);
    --bs-info-border-subtle: rgba(143, 131, 236, .28);

    /*
     * Bootstrap ships `code` at #d63384, a pink that belongs to no part of this
     * palette and reads as a sixth meaning. It was showing up wherever a screen
     * names an identifier a person can act on — `expense.payroll` on /payroll/,
     * `snapshot_networth` on /net-worth/, `ledger.view_reports` on the refusal
     * screens — which is exactly the copy that should look quiet and technical
     * rather than alarming. Mono face plus dim text does that job with no new
     * colour.
     */
    --bs-code-color: var(--ff-dim);
}

code {
    font-family: var(--ff-mono);
    font-size: .9em;
}

body {
    font-family: var(--ff-sans);
    letter-spacing: -.005em;
}

/*
 * Every digit in this app is compared against another digit, so figures are
 * monospaced and tabular everywhere rather than in the handful of places
 * somebody remembered.
 */
.ff-num,
td.text-end,
th.text-end,
.ff-figure,
.ff-stat-value {
    font-family: var(--ff-mono);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* ── Surfaces ─────────────────────────────────────────────────────────────── */

.ff-card {
    background: var(--ff-surface);
    border: 1px solid var(--ff-line-soft);
    border-radius: var(--ff-r-card);
    padding: 1.25rem 1.375rem;
}

/* The one the page is about. Sunk, not lifted. */
.ff-card--deep {
    background: var(--ff-deep);
    border-color: #191d20;
    box-shadow: var(--ff-shadow);
}

.ff-card--flush {
    padding: 0;
    overflow: hidden;
}

/* ── The headline figure ──────────────────────────────────────────────────── */

.ff-figure {
    font-size: var(--ff-t-hero);
    font-weight: 600;
    line-height: 1;
    letter-spacing: -.035em;
    color: #fff;
}

.ff-figure--sm {
    font-size: var(--ff-t-figure);
}

.ff-unit {
    font-size: var(--ff-t-small);
    color: var(--ff-faint);
    letter-spacing: .06em;
    text-transform: uppercase;
}

/* ── Labels ───────────────────────────────────────────────────────────────── */

.ff-label {
    font-size: var(--ff-t-micro);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ff-faint);
    font-weight: 500;
}

.ff-sub {
    font-size: var(--ff-t-small);
    color: var(--ff-dim);
}

.ff-section-title {
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -.012em;
    color: var(--ff-text);
    margin: 0;
}

/* ── Stat tiles ───────────────────────────────────────────────────────────── */

.ff-stat {
    background: var(--ff-surface);
    border: 1px solid var(--ff-line-soft);
    border-radius: var(--ff-r-card);
    padding: 1rem 1.125rem 1.125rem;
    position: relative;
    overflow: hidden;
    transition: border-color .25s var(--ff-ease), transform .25s var(--ff-ease);
}

/* A tint carrying meaning, not a gradient carrying decoration. */
.ff-stat::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(120% 140% at 100% -10%, var(--tint, transparent), transparent 68%);
    opacity: .9;
}

.ff-stat > * { position: relative; z-index: 1; }
.ff-stat--up { --tint: var(--ff-up-wash); }
.ff-stat--down { --tint: var(--ff-down-wash); }
.ff-stat--warn { --tint: var(--ff-warn-wash); }
.ff-stat--info { --tint: var(--ff-info-wash); }
.ff-stat--accent { --tint: var(--ff-accent-wash); }

.ff-stat-value {
    font-size: 1.75rem;
    font-weight: 600;
    line-height: 1.1;
    letter-spacing: -.03em;
    color: #fff;
    margin-top: .35rem;
}

a.ff-stat {
    display: block;
    text-decoration: none;
    color: inherit;
}

a.ff-stat:hover {
    border-color: var(--ff-line);
    transform: translateY(-1px);
}

a.ff-stat:active { transform: translateY(0); }

/* ── Chips ────────────────────────────────────────────────────────────────── */

.ff-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: var(--ff-r-pill);
    font-size: .875rem;
    flex: 0 0 auto;
}

.ff-chip--up { background: var(--ff-up-soft); color: var(--ff-up); }
.ff-chip--down { background: var(--ff-down-soft); color: var(--ff-down); }
.ff-chip--warn { background: var(--ff-warn-soft); color: var(--ff-warn); }
.ff-chip--info { background: var(--ff-info-soft); color: var(--ff-info); }
.ff-chip--accent { background: var(--ff-accent-soft); color: var(--ff-accent); }

.ff-tag {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .2rem .55rem;
    border-radius: var(--ff-r-pill);
    font-size: var(--ff-t-micro);
    font-weight: 500;
    letter-spacing: .02em;
    background: var(--ff-line-soft);
    color: var(--ff-dim);
}

.ff-tag--up { background: var(--ff-up-soft); color: var(--ff-up); }
.ff-tag--down { background: var(--ff-down-soft); color: var(--ff-down); }
.ff-tag--warn { background: var(--ff-warn-soft); color: var(--ff-warn); }
.ff-tag--accent { background: var(--ff-accent-soft); color: var(--ff-accent); }

/* ── Tables ───────────────────────────────────────────────────────────────── */

/*
 * No hairline under every row. A rule under all thirty-one rows of a month
 * gives the eye nowhere to rest and reads as a spreadsheet somebody pasted in.
 * One rule under the header, one above a total, and rows separated by the
 * space they already have.
 */
.ff-table {
    width: 100%;
    font-size: var(--ff-t-body);
}

.ff-table th {
    font-size: var(--ff-t-micro);
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ff-faint);
    font-weight: 500;
    padding: 0 .75rem .5rem;
    border-bottom: 1px solid var(--ff-line);
}

.ff-table td {
    padding: .6rem .75rem;
    border: 0;
    vertical-align: middle;
}

.ff-table tbody tr {
    transition: background-color .18s var(--ff-ease);
}

.ff-table tbody tr:hover {
    background: rgba(255, 255, 255, .022);
}

.ff-table .ff-row-total td {
    border-top: 1px solid var(--ff-line);
    font-weight: 600;
    color: #fff;
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */

.ff-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    /* 44px reach. A control in a finance app gets tapped on a phone too. */
    min-height: 2.75rem;
    padding: 0 1.05rem;
    border-radius: var(--ff-r-pill);
    border: 1px solid transparent;
    font-size: var(--ff-t-small);
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background-color .2s var(--ff-ease), border-color .2s var(--ff-ease), transform .12s var(--ff-ease);
}

.ff-btn--primary {
    background: var(--ff-accent);
    color: var(--ff-on-accent);
}

.ff-btn--primary:hover { background: #d6ff70; color: var(--ff-on-accent); }

.ff-btn--quiet {
    background: transparent;
    border-color: var(--ff-line);
    color: var(--ff-text);
}

.ff-btn--quiet:hover { border-color: var(--ff-faint); color: #fff; }

.ff-btn:active { transform: scale(.985); }

/*
 * Bootstrap writes its own literal hex into every `.btn-*` rather than reading
 * `--bs-primary`, so re-pointing the theme variable recoloured links and left
 * every primary button the stock blue. These have to be said out loud.
 */
.btn-primary {
    --bs-btn-bg: var(--ff-accent);
    --bs-btn-border-color: var(--ff-accent);
    --bs-btn-color: var(--ff-on-accent);
    --bs-btn-hover-bg: #d6ff70;
    --bs-btn-hover-border-color: #d6ff70;
    --bs-btn-hover-color: var(--ff-on-accent);
    --bs-btn-active-bg: #b8e844;
    --bs-btn-active-border-color: #b8e844;
    --bs-btn-active-color: var(--ff-on-accent);
    --bs-btn-disabled-bg: var(--ff-line);
    --bs-btn-disabled-border-color: var(--ff-line);
    --bs-btn-disabled-color: var(--ff-faint);
}

.btn-outline-secondary {
    --bs-btn-color: var(--ff-text);
    --bs-btn-border-color: var(--ff-line);
    --bs-btn-hover-bg: var(--ff-line-soft);
    --bs-btn-hover-border-color: var(--ff-faint);
    --bs-btn-hover-color: #fff;
}

/* Bootstrap buttons in this app get the same 44px reach as everything else. */
.btn {
    --bs-btn-border-radius: var(--ff-r-pill);
    min-height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
}

.btn-sm { min-height: 2.25rem; }

.form-control, .form-select {
    background-color: var(--ff-deep);
    border-color: var(--ff-line);
    color: var(--ff-text);
    min-height: 2.75rem;
}

.form-control:focus, .form-select:focus {
    background-color: var(--ff-deep);
    border-color: var(--ff-accent-line);
    color: var(--ff-text);
    box-shadow: 0 0 0 3px var(--ff-accent-soft);
}

.form-control::placeholder { color: var(--ff-faint); }

/*
 * The keyboard ring.
 *
 * It used to name five selectors, which left every bare `<input>`, every
 * `<select>` that had not been given a Bootstrap class, every `<summary>` — and
 * `<summary>` is how the chart settings, the exact-date pickers and the
 * drilldowns all open — with nothing but whatever the browser felt like drawing
 * on a dark field, which in Chrome is a thin black line you cannot see.
 *
 * Two rings, not one. The dark inner ring is what makes the lime visible when
 * the focused thing is itself lime: a lime outline on a lime button is a button
 * with no focus state at all, and the primary action on four screens is lime.
 */
:is(a, button, summary, input, select, textarea, [tabindex]):focus-visible,
.ff-btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible {
    /* An outline follows the element's own border-radius, so a pill is ringed
       as a pill and a card as a card without any of them being named here. The
       2px offset is what makes the ring readable on a lime button: the gap
       shows the dark ground the button is sitting on. */
    outline: 2px solid var(--ff-accent);
    outline-offset: 2px;
}

/* A disclosure triangle has no box of its own to ring, so the ring goes round
   the control rather than round the word. */
summary:focus-visible { outline-offset: 0; }

/* ── Meters ───────────────────────────────────────────────────────────────── */

/*
 * No filled background track. A grey trough behind every bar turns a page of
 * proportions into a page of grey troughs; the bar alone carries the length.
 */
.ff-meter {
    height: 4px;
    border-radius: var(--ff-r-pill);
    background: var(--ff-line-soft);
    overflow: hidden;
}

.ff-meter > span {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--ff-accent);
}

/* ── Motion ───────────────────────────────────────────────────────────────── */

/*
 * One movement, and it earns its place: HTMX replaces the whole of `main` on
 * every navigation, and without a transition the swap reads as a flicker that
 * makes a fast page feel broken.
 */
@media (prefers-reduced-motion: no-preference) {
    main > * {
        animation: ff-enter .32s var(--ff-ease) both;
    }
}

@keyframes ff-enter {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}

/*
 * And the other half of that bargain, said once for the whole app rather than
 * remembered per component. Everything that moves here — the swap, the strip
 * driving itself to the current screen, the count-up on the headline figure,
 * the hover and focus transitions — is off for anyone whose system asks for it.
 *
 * The count-up is checked in JavaScript as well, because a transition you have
 * disabled still ends on the right value while a counter you have disabled has
 * to skip to it.
 */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ── Scrollable regions ───────────────────────────────────────────────────── */

/*
 * No fade at the right edge.
 *
 * It was there as a scroll affordance, back when a wide table on a phone really
 * did scroll sideways. Tables stack now, so nothing scrolls, and all the fade
 * did was wash out the last digit of every right-aligned figure: `56,499` was
 * rendering with the final 9 half gone. CSS cannot ask whether an element is
 * actually overflowing, so a fade applied on the chance that it might is a fade
 * that eats real numbers most of the time. In a finance tool, a figure you have
 * to squint at is worse than a scrollbar you have to find.
 */
.table-responsive {
    scrollbar-width: thin;
    scrollbar-color: var(--ff-line) transparent;
}


/* ── Page furniture ───────────────────────────────────────────────────────── */

.ff-page {
    padding-top: 1.25rem;
    padding-bottom: 3rem;
}

.ff-hero {
    padding: 1.5rem 1.625rem 1.25rem;
}

.ff-hero-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 1rem;
    flex-wrap: wrap;
    /* Clear of the settings control, which is absolutely positioned in the same
       corner. Without this the change tag runs underneath it and loses its
       percentage, which is the half of it worth reading. */
    padding-right: 3rem;
}

/* ── The trend chart ──────────────────────────────────────────────────────── */

/*
 * A grid, not a block, and that is the whole fix for the low mark.
 *
 * The marks and the dot are positioned in percentages of the plot — `top: 95%`
 * for the low, which is where the trough always sits. They were resolving
 * against the *figure*, which is the plot plus the gap plus the caption: 168px
 * where the chart is 120. So `95%` meant 160px instead of 114 and the low label
 * landed 46px too far down, over the date row and 3px past the bottom of the
 * card. The end dot was off its own line for the same reason, quietly.
 *
 * An absolutely positioned child of a grid container that names a grid area is
 * positioned against *that area*, not the container. So the plot gets row one,
 * the caption gets row two, the marks and the dot join the plot, and every
 * percentage in the template now means what it says.
 *
 * The gap is a `row-gap` rather than a margin on the svg for the same reason:
 * a margin would be inside row one and the percentages would be wrong again.
 */
.ff-chart {
    --ff-spark: var(--ff-up);
    position: relative;
    margin-top: 1.25rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    /* Room under the curve for the low mark, which labels the very bottom of
       the plot and would otherwise land on the caption. */
    row-gap: 1.75rem;
}

.ff-chart--down { --ff-spark: var(--ff-down); }

.ff-chart svg {
    display: block;
    width: 100%;
    height: 120px;
    grid-row: 1;
    grid-column: 1;
}

/*
 * Bound here rather than on the elements: `var()` does not resolve inside an
 * SVG presentation attribute, so `stroke="var(--ff-spark)"` silently renders
 * black and the chart disappears against the card.
 */
.ff-chart .ff-spark-line { stroke: var(--ff-spark); }
.ff-chart .ff-spark-stop { stop-color: var(--ff-spark); }

/* The last reading, marked. A line that simply stops reads as truncated. */
.ff-chart-dot {
    position: absolute;
    grid-area: 1 / 1 / 2 / 2;
    width: 9px;
    height: 9px;
    border-radius: var(--ff-r-pill);
    background: var(--ff-spark);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--ff-spark) 22%, transparent);
    transform: translate(-50%, -50%);
    pointer-events: none;
}

.ff-chart-foot {
    grid-row: 2;
    grid-column: 1;
    display: flex;
    justify-content: space-between;
    font-size: var(--ff-t-micro);
    color: var(--ff-faint);
    padding-top: .5rem;
    letter-spacing: .04em;
}

/* ── Stat grid ────────────────────────────────────────────────────────────── */

.ff-stat-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: .75rem;
}

@media (max-width: 767.98px) {
    .ff-stat-grid { grid-template-columns: repeat(2, 1fr); }
    .ff-figure { font-size: 3rem; }
    .ff-hero { padding: 1.25rem; }
    /* Two to a row at 390px only fits if the figure gives a little back. */
    .ff-stat { padding: .875rem .875rem 1rem; }
    .ff-stat-value { font-size: 1.375rem; }
}

@media (max-width: 359.98px) {
    .ff-stat-grid { grid-template-columns: 1fr; }
}

/* ── Notes ────────────────────────────────────────────────────────────────── */

/*
 * An alert that is a coloured slab shouts the same volume whatever it says. A
 * chip plus a left edge lets severity be read at a glance without the whole row
 * becoming a warning sign.
 */
.ff-note {
    display: flex;
    gap: .875rem;
    align-items: flex-start;
    border-left: 3px solid var(--ff-line);
    padding: 1rem 1.25rem;
}

.ff-note--down { border-left-color: var(--ff-down); }
.ff-note--warn { border-left-color: var(--ff-warn); }
.ff-note--up { border-left-color: var(--ff-up); }
.ff-note--info { border-left-color: var(--ff-info); }

/* ── Share bars ───────────────────────────────────────────────────────────── */

/* The column the share bar lives in: wide enough for the bar and its number,
   and right-aligned so the percentages form their own edge. */
.ff-share-col {
    text-align: right;
    width: 1%;
    white-space: nowrap;
}

.ff-share {
    display: flex;
    align-items: center;
    gap: .6rem;
    justify-content: flex-end;
}

.ff-share-track {
    width: 104px;
    flex: 0 0 104px;
    /* No fill behind it. The bar carries the length; a trough under every row
       turns a page of proportions into a page of troughs. */
}

.ff-share-bar {
    display: block;
    height: 4px;
    min-width: 3px;
    border-radius: var(--ff-r-pill);
    background: var(--ff-accent);
}

.ff-share-num {
    font-family: var(--ff-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--ff-t-small);
    color: var(--ff-dim);
    width: 3.25rem;
    text-align: right;
}

@media (max-width: 767.98px) {
    /* The bar is the first thing to go: the number says it already. */
    .ff-share-track { display: none; }
}

.ff-neg { color: var(--ff-down); }
.ff-warn-text { color: var(--ff-warn); }


/* ═══ Components ══════════════════════════════════════════════════════════ */

/* ── Page header ──────────────────────────────────────────────────────────── */

.ff-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.ff-head h1 {
    font-size: 1.5rem;
    font-weight: 600;
    letter-spacing: -.022em;
    margin: 0;
    color: #fff;
}

.ff-head-note {
    font-size: var(--ff-t-small);
    color: var(--ff-dim);
    margin: .3rem 0 0;
    max-width: 68ch;
}

/* ── Segmented control ────────────────────────────────────────────────────── */

/*
 * For a choice of one from a small fixed set: a period, a view, a month. Reads
 * as one control rather than as a row of loose links, which is what a set of
 * mutually exclusive options actually is.
 */
.ff-seg {
    display: inline-flex;
    padding: .2rem;
    gap: .125rem;
    background: var(--ff-line-soft);
    border-radius: var(--ff-r-pill);
}

.ff-seg a,
.ff-seg button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Comfortable for a mouse. A finger needs more, which the touch rule
       below gives it: 2.125rem is 34px and the floor for a tap target is 44. */
    min-height: 2.125rem;
    padding: 0 .85rem;
    border: 0;
    background: transparent;
    border-radius: var(--ff-r-pill);
    color: var(--ff-dim);
    font-size: var(--ff-t-small);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: color .2s var(--ff-ease), background-color .2s var(--ff-ease);
}

.ff-seg a:hover,
.ff-seg button:hover { color: var(--ff-text); }

/*
 * Pointer-based, not width-based. A small laptop with a trackpad does not need
 * the taller control, and a large tablet does, so the question is what is
 * pointing at it rather than how wide the window happens to be.
 */
@media (hover: none) and (pointer: coarse) {
    .ff-seg a,
    .ff-seg button { min-height: 2.75rem; }
}

.ff-seg .is-current,
.ff-seg [aria-selected="true"] {
    background: var(--ff-surface);
    color: var(--ff-text);
    font-weight: 600;
    box-shadow: 0 1px 2px rgba(6, 8, 9, .4);
}

/* ── Progress ring ────────────────────────────────────────────────────────── */

/*
 * One proportion, given its own weight. A bar buried in a table says the same
 * thing, but a figure the business is meant to act on deserves to be the shape
 * the eye finds first.
 */
.ff-ring {
    --value: 0;
    --ring: var(--ff-accent);
    position: relative;
    width: 116px;
    height: 116px;
    border-radius: var(--ff-r-pill);
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    background: conic-gradient(var(--ring) calc(var(--value) * 1%), var(--ff-line-soft) 0);
}

/* Punched out rather than drawn, so the track and the fill cannot disagree. */
.ff-ring::before {
    content: "";
    position: absolute;
    inset: 11px;
    border-radius: inherit;
    background: var(--ff-surface);
}

/* On the sunk card the hole has to be sunk too, or the ring reads as a badge
   stuck on top rather than a hole cut through. */
.ff-card--deep .ff-ring::before { background: var(--ff-deep); }

.ff-ring-inner {
    position: relative;
    text-align: center;
    line-height: 1.15;
}

.ff-ring-value {
    font-family: var(--ff-mono);
    font-variant-numeric: tabular-nums;
    font-size: 1.375rem;
    font-weight: 600;
    color: #fff;
    letter-spacing: -.03em;
}

.ff-ring-label {
    font-size: var(--ff-t-micro);
    color: var(--ff-faint);
    text-transform: uppercase;
    letter-spacing: .08em;
}

/* ── List rows ────────────────────────────────────────────────────────────── */

/*
 * For a run of things that each have an icon, a name and a value. Rows are told
 * apart by their own space and a hover, never by a rule under every one of
 * them, which is the laziest layout there is and reads as a pasted spreadsheet.
 */
.ff-list {
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.ff-list-item {
    display: flex;
    align-items: center;
    gap: .875rem;
    padding: .625rem .75rem;
    border-radius: var(--ff-r-control);
    text-decoration: none;
    color: inherit;
    transition: background-color .18s var(--ff-ease);
}

a.ff-list-item:hover { background: var(--ff-line-soft); }

/*
 * `flex: 1 1 0`, not `auto`. With an auto basis the body resolves its width from
 * its own content, so a long title pushes the whole body onto a second line and
 * strands the chip above it. A zero basis lets it take what is left and wrap
 * inside itself, which is what a row of icon, name and value should do.
 */
.ff-list-body { flex: 1 1 0; min-width: 0; }

.ff-list-title {
    font-weight: 600;
    color: var(--ff-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ff-list-note {
    font-size: var(--ff-t-small);
    color: var(--ff-dim);
}

.ff-list-value {
    font-family: var(--ff-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* ── The empty screen ─────────────────────────────────────────────────────── */

/*
 * A screen with nothing on it yet is the one screen guaranteed to be somebody's
 * first, and "No items." is the worst possible thing to say on it: it reports a
 * fact and withholds the only thing the person came for, which is what to do
 * next. Inventory got this right first — a numbered run of steps, each with the
 * sentence that explains why the step exists and the button that does it — so
 * that pattern is a primitive here rather than one screen's private CSS.
 *
 *   <section class="ff-card">
 *     <h2 class="ff-section-title mb-3">Putting the first thing in</h2>
 *     <div class="ff-list ff-steps">
 *       <div class="ff-list-item">
 *         <span class="ff-chip ff-chip--accent" aria-hidden="true">1</span>
 *         <div class="ff-list-body">
 *           <div class="ff-list-title">Make a category and a location</div>
 *           <div class="ff-list-note">Why this step exists, in one sentence.</div>
 *         </div>
 *         <a class="ff-btn ff-btn--quiet" href="…">Categories</a>
 *       </div>
 *       …
 *     </div>
 *   </section>
 *
 * Three rules to keep: number the steps, give exactly one of them the primary
 * button (the step that is actually the point), and write the note as the
 * reason rather than the instruction repeated.
 */
.ff-steps .ff-list-title {
    /* `.ff-list` ellipsises its titles, which is right for a run of names and
       wrong for a run of instructions: "Make a category …" is not a step. */
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}

.ff-steps .ff-list-item { align-items: flex-start; }

/* The number sits on the first line of the title, not centred against a
   two-line body. */
.ff-steps > .ff-list-item > .ff-chip { margin-top: .1rem; }

@media (max-width: 575.98px) {
    .ff-steps .ff-list-item { flex-wrap: wrap; }

    /* A flex basis, not a min-width. `flex: 1 1 auto` resolves its basis from
       the content, which for a sentence is far wider than the row, so the body
       wraps to its own line and strands the number above it. A fixed basis
       keeps the number and the text together and sends only the button down. */
    .ff-steps .ff-list-body { flex: 1 1 12rem; }

    /* Aligned under the text, not under the number, so the step reads as one
       block: chip 2rem plus the row's own gap and padding. */
    .ff-steps .ff-btn { margin-left: 3.65rem; }
}

/* ── Layout ───────────────────────────────────────────────────────────────── */

/*
 * Main plus a side panel, as in a dashboard that answers one big question on
 * the left and keeps the running detail on the right. Collapses to one column
 * before the side panel gets too narrow to hold a number and its label.
 */
.ff-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: .75rem;
    align-items: start;
}

@media (max-width: 1199.98px) {
    .ff-split { grid-template-columns: minmax(0, 1fr); }
}

.ff-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .75rem; }
.ff-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; }

@media (max-width: 767.98px) {
    .ff-grid-2,
    .ff-grid-3 { grid-template-columns: minmax(0, 1fr); }
}

/* ── Phone ────────────────────────────────────────────────────────────────── */

@media (max-width: 767.98px) {
    .ff-page { padding-top: .75rem; }
    .ff-card { padding: 1rem 1.125rem; border-radius: 14px; }
    .ff-head h1 { font-size: 1.25rem; }
    .ff-ring { width: 96px; height: 96px; }
    .ff-ring::before { inset: 9px; }

    /*
     * A table that has to scroll sideways on a phone is a table with hidden
     * columns. Where the markup offers a stacked form, use it.
     */
    .ff-table th,
    .ff-table td { padding: .55rem .5rem; }
}


/* ── Tables that stack ────────────────────────────────────────────────────── */

/*
 * A table that only scrolls sideways on a phone is a table with hidden columns,
 * and the reader has no way of knowing what they are missing. Below 768px this
 * turns each row into its own block and moves the column heading beside its own
 * value, so nothing is lost and nothing needs a horizontal drag.
 *
 * Every cell needs `data-label` for its heading. A cell whose heading is the row
 * itself (the name column) takes `data-label=""` and keeps the full width.
 */
@media (max-width: 767.98px) {
    .ff-table--stack thead {
        /* Hidden visually; the labels move into the cells below. */
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    .ff-table--stack tbody tr {
        display: block;
        padding: .75rem .25rem;
        border-radius: var(--ff-r-control);
    }

    .ff-table--stack tbody tr + tr {
        border-top: 1px solid var(--ff-line-soft);
    }

    .ff-table--stack td {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 1rem;
        padding: .2rem .5rem;
        text-align: right;
    }

    .ff-table--stack td::before {
        content: attr(data-label);
        font-size: var(--ff-t-micro);
        letter-spacing: .08em;
        text-transform: uppercase;
        color: var(--ff-faint);
        text-align: left;
        flex: 0 0 auto;
    }

    /* The row's own name: no label, full width, and it leads. */
    .ff-table--stack td[data-label=""] {
        justify-content: flex-start;
        text-align: left;
        font-weight: 600;
        padding-bottom: .4rem;
    }

    .ff-table--stack td[data-label=""]::before { content: none; }

    /* An empty cell says nothing worth a row of its own on a small screen. */
    .ff-table--stack td:empty { display: none; }

    /*
     * Nor does a cell repeating the one below it. Marked in the template rather
     * than guessed here, because only the view knows when two figures are the
     * same number rather than merely the same width.
     */
    .ff-table--stack td.ff-same-as-net { display: none; }

    /* A footer total is still a label and a figure, so it reads like one. */
    .ff-table--stack tfoot tr { display: flex; justify-content: space-between; align-items: baseline; }
    .ff-table--stack tfoot td { display: block; text-align: left; }
    .ff-table--stack tfoot td:last-child { text-align: right; }
    .ff-table--stack tfoot td::before { content: none; }
}


/* ── Chart settings ───────────────────────────────────────────────────────── */

/*
 * The gear sits over the card's top-right corner and the panel opens below the
 * chart, so nothing is ever positioned off the side of a phone and the layout
 * only ever grows downward.
 */
.ff-hero { position: relative; }

.ff-settings > summary {
    position: absolute;
    top: 1rem;
    right: 1rem;
    /* 44px of reach, and no marker: the native triangle is a 7px glyph that
       reads as a bullet rather than a control. */
    list-style: none;
    width: 2.5rem;
    height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--ff-r-control);
    color: var(--ff-faint);
    cursor: pointer;
    transition: color .2s var(--ff-ease), background-color .2s var(--ff-ease);
}

.ff-settings > summary::-webkit-details-marker,
.ff-settings > summary::marker { display: none; content: ""; }

.ff-settings > summary:hover { color: var(--ff-text); background: var(--ff-line-soft); }
.ff-settings[open] > summary { color: var(--ff-on-accent); background: var(--ff-accent); }

.ff-settings-panel {
    margin-top: 1.25rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--ff-line-soft);
}

.ff-settings-dates {
    display: flex;
    gap: .75rem;
    align-items: flex-end;
    flex-wrap: wrap;
}

.ff-settings-dates label {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    flex: 1 1 10rem;
    min-width: 0;
}

@media (max-width: 575.98px) {
    /* Two date pickers side by side at 390px leaves neither of them readable. */
    .ff-settings-dates label { flex: 1 1 100%; }
    .ff-settings-dates .ff-btn { width: 100%; }
    .ff-settings > summary { top: .75rem; right: .75rem; }
}

/* ── The high and low marks ───────────────────────────────────────────────── */

/*
 * Deliberately quiet. They label the line, they are not competing with the
 * headline figure, and two loud numbers on a small chart would be worse than
 * none. The transform lifts the high above its point and drops the low below,
 * so neither sits on the stroke it is describing.
 */
.ff-chart-mark {
    position: absolute;
    /* Against the plot, not against the figure. See the note on `.ff-chart`.
       Both ends of both lines have to be named: `grid-row: 1` alone leaves the
       end line `auto`, which for an absolutely positioned item means the
       container's padding edge — so the area would still be the whole figure
       and the percentage would still be wrong. */
    grid-area: 1 / 1 / 2 / 2;
    display: flex;
    flex-direction: column;
    line-height: 1.1;
    font-size: var(--ff-t-micro);
    color: var(--ff-faint);
    pointer-events: none;
    white-space: nowrap;
    transform: translate(-50%, 0);
}

.ff-chart-mark b {
    font-family: var(--ff-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    color: var(--ff-dim);
}

.ff-chart-mark i { font-style: normal; opacity: .7; }
.ff-chart-mark--high { transform: translate(-50%, calc(-100% - 6px)); }
.ff-chart-mark--low { transform: translate(-50%, 6px); }

/* Against the card edges the label would be clipped, so it tucks inward. */
.ff-chart-mark[style*="left: 0%"] { transform: translate(0, 0); }
.ff-chart-mark--high[style*="left: 0%"] { transform: translate(0, calc(-100% - 6px)); }
.ff-chart-mark--low[style*="left: 0%"] { transform: translate(0, 6px); }
.ff-chart-mark[style*="left: 100%"] { transform: translate(-100%, 0); }
.ff-chart-mark--high[style*="left: 100%"] { transform: translate(-100%, calc(-100% - 6px)); }
.ff-chart-mark--low[style*="left: 100%"] { transform: translate(-100%, 6px); }

/* The foot now carries three things, so it spreads rather than splits. */
.ff-chart-foot { gap: 1rem; }
.ff-chart-foot span:nth-child(2) { flex: 1 1 auto; text-align: center; opacity: .7; }

@media (max-width: 575.98px) {
    /* The reading count is the first thing to go when there is no room. */
    .ff-chart-foot span:nth-child(2) { display: none; }

    /*
     * And the mark loses its date before it loses its number. Two lines under a
     * 120px plot leaves the low label sitting on the caption; one line clears
     * it, and the figure is the part worth keeping.
     */
    .ff-chart-mark i { display: none; }
}

/* ── Waiting ───────────────────────────────────────────────────────────────
 *
 * Every server round-trip has to show that it is working, within 100ms, where
 * the person is already looking. See the `ux` skill, §5.
 *
 * The load-bearing decision here is that this is keyed on htmx's own
 * `htmx-request` class rather than on markup somebody has to remember to add.
 * A per-element spinner is a rule; a global one is a guarantee. Adding a new
 * button to this product now gets a loading state whether or not its author
 * thought about it.
 */

@keyframes ff-spin { to { transform: rotate(360deg); } }

/* Appears only if the wait is real. Under ~150ms nothing is drawn at all,
 * because a spinner that flashes and vanishes reads as a glitch and makes a
 * fast app feel unstable. */
@keyframes ff-wait-in { from { opacity: 0; } to { opacity: 1; } }

/*
 * A control that is currently talking to the server: it stops accepting
 * presses, dims, and grows a spinner where its own trailing edge is.
 */
.ff-btn.htmx-request,
.ff-seg a.htmx-request,
.ff-seg button.htmx-request,
.ff-strip a.htmx-request,
.ff-list-item.htmx-request,
.htmx-request > .ff-btn {
    pointer-events: none;
    position: relative;
}

.ff-btn.htmx-request::after,
.ff-seg a.htmx-request::after,
.ff-seg button.htmx-request::after,
.ff-strip a.htmx-request::after,
.ff-list-item.htmx-request::after {
    content: "";
    display: inline-block;
    width: .85em;
    height: .85em;
    margin-left: .5rem;
    vertical-align: -.1em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    opacity: 0;
    animation: ff-spin .6s linear infinite, ff-wait-in .12s linear 150ms forwards;
}

/* The label fades rather than moving: a button that changes width mid-press
 * moves the thing under the finger that is still on it. */
.ff-btn.htmx-request { opacity: .72; }

/*
 * A skeleton: the shape of the answer, in the space the answer will occupy.
 * Beats a centred spinner because the eye has structure to read while it waits,
 * and because nothing jumps when the content lands — the room was already the
 * right size.
 */
.ff-skeleton {
    position: relative;
    overflow: hidden;
    border-radius: var(--ff-r-control);
    background: var(--ff-line-soft);
    opacity: 0;
    animation: ff-wait-in .2s linear 150ms forwards;
}

.ff-skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .05), transparent);
    animation: ff-sweep 1.4s ease-in-out infinite;
}

@keyframes ff-sweep { to { transform: translateX(100%); } }

.ff-skeleton--text { height: .75rem; border-radius: var(--ff-r-pill); }
.ff-skeleton--line { height: 1.15rem; border-radius: var(--ff-r-pill); }
.ff-skeleton--figure { height: 3.25rem; width: 12rem; }
.ff-skeleton--chart { height: 9rem; }
.ff-skeleton--card { height: 6.5rem; border-radius: var(--ff-r-card); }

/* Skeleton rows read as content, so they need the gaps content has. */
.ff-skeleton-stack { display: grid; gap: .6rem; }
.ff-skeleton-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: .75rem;
}

/*
 * Motion is the affordance, so without motion the state still has to be
 * legible: the placeholder simply sits there, visibly not-yet-content.
 */
@media (prefers-reduced-motion: reduce) {
    .ff-btn.htmx-request::after,
    .ff-seg a.htmx-request::after,
.ff-seg button.htmx-request::after,
    .ff-strip a.htmx-request::after,
    .ff-list-item.htmx-request::after {
        animation: ff-wait-in .12s linear 150ms forwards;
        border-right-color: currentColor;
        opacity: .5;
    }

    .ff-skeleton { animation: none; opacity: 1; }
    .ff-skeleton::after { animation: none; }
}

/*
 * The spinner for a region whose shape we cannot honestly predict. Sized and
 * weighted to sit calmly in an empty content column rather than to demand
 * attention: it is saying "a moment", not "something is wrong".
 */
.ff-wait {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40vh;
    opacity: 0;
    animation: ff-wait-in .2s linear 150ms forwards;
}

.ff-wait-ring {
    width: 2rem;
    height: 2rem;
    border: 2px solid var(--ff-line);
    border-top-color: var(--ff-accent);
    border-radius: 50%;
    animation: ff-spin .7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .ff-wait-ring {
        animation: none;
        border-top-color: var(--ff-accent);
        opacity: .6;
    }
}

/*
 * A skeleton block that stands in for a card, so the outline the reader sees is
 * the outline that arrives: same surface, same border, same radius.
 */
.ff-skeleton--surface {
    background: var(--ff-surface);
    border: 1px solid var(--ff-line-soft);
    border-radius: var(--ff-r-card);
}

.ff-skeleton--deep {
    background: var(--ff-deep);
    border: 1px solid var(--ff-line-soft);
    border-radius: var(--ff-r-card);
}

/* ── Reading a chart with the cursor ───────────────────────────────────────
 *
 * A line tells you the shape and refuses to tell you the number. These put the
 * number back: a rule under the pointer, the date at the foot of it, and the
 * value where the line actually is. Quiet by default — the chart is still the
 * thing being looked at — and gone the moment the pointer leaves.
 *
 * Positioned in percentages of the plot, so they follow the SVG as it scales
 * and never need measuring in script.
 */

.ff-probe-line,
.ff-probe-dot,
.ff-probe-value,
.ff-probe-date {
    position: absolute;
    pointer-events: none;
    z-index: 3;
}

/* Against the plot cell, not the whole figure — the same trap the high and low
   marks fell into. Both ends of both grid lines must be named. */
.ff-chart .ff-probe-line,
.ff-chart .ff-probe-dot,
.ff-chart .ff-probe-value,
.ff-chart .ff-probe-date { grid-area: 1 / 1 / 2 / 2; }

.ff-probe-line {
    top: 0;
    bottom: 0;
    width: 1px;
    margin-left: -0.5px;
    background: linear-gradient(var(--ff-line), var(--ff-faint), var(--ff-line));
    opacity: .9;
}

.ff-probe-dot {
    width: .5rem;
    height: .5rem;
    margin: -0.25rem 0 0 -0.25rem;
    border-radius: 50%;
    background: var(--ff-spark, var(--ff-accent));
    box-shadow: 0 0 0 3px var(--ff-bg);
}

.ff-probe-dot--in { background: var(--ff-accent); }
.ff-probe-dot--out { background: var(--ff-dim); }

/* The figure, set on the line it belongs to. Nudged up so the dot stays visible
   underneath it, and flipped to the other side near the right edge so it never
   runs out of the plot. */
.ff-probe-value {
    transform: translate(-50%, calc(-100% - .55rem));
    padding: .1rem .4rem;
    border-radius: var(--ff-r-control);
    background: var(--ff-deep);
    border: 1px solid var(--ff-line);
    font-family: var(--ff-mono);
    font-size: var(--ff-t-micro);
    font-variant-numeric: tabular-nums;
    color: var(--ff-text);
    white-space: nowrap;
}

.ff-probe-value b { font-weight: 600; }
.ff-probe-value i { font-style: normal; color: var(--ff-faint); }

/* The date sits at the foot of the rule, in the gap the chart already reserves
   under the plot, so it displaces nothing. */
.ff-probe-date {
    top: 100%;
    transform: translate(-50%, .35rem);
    font-family: var(--ff-mono);
    font-size: var(--ff-t-micro);
    letter-spacing: .04em;
    color: var(--ff-dim);
    white-space: nowrap;
}

.ff-probe-value--flip { transform: translate(calc(-100% + .5rem), calc(-100% - .55rem)); }
.ff-probe-date--flip { transform: translate(calc(-100% + .5rem), .35rem); }
.ff-probe-value--start { transform: translate(-.5rem, calc(-100% - .55rem)); }
.ff-probe-date--start { transform: translate(-.5rem, .35rem); }

/* While a chart is being read, its own end-of-line marks would collide with the
   probe, so they step back. */
.ff-probe.is-probing .ff-chart-mark,
.ff-probe.is-probing .ff-chart-dot,
.ff-probe.is-probing .ff-flow-dot,
.ff-probe.is-probing .ff-flow-mark { opacity: .18; transition: opacity .15s var(--ff-ease); }

@media (prefers-reduced-motion: reduce) {
    .ff-probe.is-probing .ff-chart-mark,
    .ff-probe.is-probing .ff-chart-dot,
    .ff-probe.is-probing .ff-flow-dot,
    .ff-probe.is-probing .ff-flow-mark { transition: none; }
}


/* ── A step in a line that no trading explains ────────────────────────────── */

/*
 * The books record a correction on the day they *learn* it, never the day it was
 * true, because nothing here is ever backdated. So a net worth line carries jumps
 * that no business activity accounts for, and without a mark a reader is left to
 * conclude the company earned eighty thousand dollars on a day it sold nothing.
 *
 * Shared by every chart that draws a balance over time, which is why it lives here
 * rather than in one screen's stylesheet.
 *
 * Info rather than warning: money turning up is a fact about when the books
 * learned something, not a fault. The rule is permanent because it costs nothing
 * and answers "is there something here". The caption is not, because a label
 * nailed to a chart somebody reads every week stops being information and becomes
 * furniture; it appears when the crosshair reaches that reading.
 */
.ff-step-line {
    stroke: var(--ff-info);
    stroke-width: 1;
    stroke-dasharray: 1 4;
}

.ff-step-mark {
    position: absolute;
    /* Lifted clear of the plot's foot. Its mask is the card's own colour, so
       sitting on the boundary the label dissolves into the card below and reads
       as a smudge rather than a chip. */
    bottom: .7rem;
    transform: translateX(.4rem);
    display: flex;
    flex-direction: column;
    z-index: 4;
    max-width: 13rem;
    /* Masked: this sits low and wide, and other markers rule the full height of a
       plot. Without it their lines draw straight through the words. */
    padding: .25rem .45rem;
    border-radius: var(--ff-r-control);
    background: var(--ff-surface);
    /* And an edge, so it stays a chip wherever it lands: over a filled band, over
       bare plot, or over whatever a future chart puts underneath it. */
    border: 1px solid var(--ff-line);
    font-size: var(--ff-t-micro);
    line-height: 1.3;
    pointer-events: none;
}

.ff-step-mark--right { transform: translateX(calc(-100% - .4rem)); text-align: right; }
.ff-step-mark b { color: var(--ff-info); font-weight: 600; }
.ff-step-mark i { color: var(--ff-faint); font-style: normal; }

@media (max-width: 575.98px) {
    /* The rule still marks the day; the sentence does not fit beside it. */
    .ff-step-mark i { display: none; }
}
