/* IndieWallet — local royalty tracker. Single hand-written stylesheet, no
   framework. Near-black ink on bone paper, one olive accent, one grey.

   Four hues and nothing else: every colour in this file is one of those four, a
   tint or shade of one, or one of them at partial alpha. That rule is the
   reason the olive shows up so rarely — it is the only chromatic value on the
   page, so spending it on anything ordinary would stop it reading as an accent.

   Two typefaces, both sitting in ./fonts rather than on a CDN. This app has to
   work with the wifi off, and a Google Fonts link fails silently when it can't
   load: no error, no warning, the page just renders in Times and the design is
   gone. Self-hosting costs 124K and removes the failure mode entirely. */

/* Variable fonts, latin + latin-ext. Two files per family rather than one per
   weight — the whole range costs less than three static cuts would, and the
   design leans on weight contrast, so it would have needed those cuts.
   latin-ext is here for artist names: royalty statements are full of ö, é and
   ñ, and a missing glyph in somebody's name is not a rounding error. */
@font-face {
  font-family: 'Libre Bodoni';
  src: url('fonts/librebodoni-latin.woff2') format('woff2');
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Libre Bodoni';
  src: url('fonts/librebodoni-latinext.woff2') format('woff2');
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Libre Bodoni';
  src: url('fonts/librebodoni-italic-latin.woff2') format('woff2');
  font-weight: 400 700; font-style: italic; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Public Sans';
  src: url('fonts/publicsans-latin.woff2') format('woff2');
  font-weight: 300 800; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Public Sans';
  src: url('fonts/publicsans-latinext.woff2') format('woff2');
  font-weight: 300 800; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* The four. Everything else in this file derives from these.

     The paper is bone: 23% saturation at 91% lightness. That is enough warmth
     to read as paper stock and not enough to read as a pastel. It is also
     deliberately not white — neumorphism needs room for a highlight *above* the
     surface as well as a shadow below it, and there is nowhere above white.

     The olive measures 3.9:1 against the paper. That clears the 3:1 a shape or
     a control edge needs and misses the 4.5:1 a word needs, which is the whole
     reason it is a fill and never a typeface. Every word on the site is ink. */
  --paper: #EDEAE1;   /* the ground. 14.73:1 against the ink */
  --ink:   #1A1815;   /* near-black, warm. all type */
  --olive: #6B7A3F;   /* the pop. bars, marks, rules, the mascot. never text */
  --grey:  #5E5A4F;   /* subtext. 6.0:1 on paper, 5.4:1 on the sunken fill */

  /* Tints and shades of the paper. All three sit within 1.25:1 of it, which is
     the point — they separate surfaces without ever becoming a second colour.
     The lift is for things that float above the page; the sunken pair is for
     things you type into, or that sit behind data. */
  --paper-hi:   #F5F3EE;  /* 1.08 — floats: dialog, popover, hovered row */
  --paper-lo:   #E2DFD7;  /* 1.11 — sunken: inputs, table heads, tracks */
  --paper-deep: #D5D3CA;  /* 1.25 — deepest recess, disabled fills */

  /* Shades of the olive. --deep is dark enough to read as text (6.35:1) even
     though nothing uses it that way; it exists so a hovered olive shape gets
     darker rather than lighter, because lightening an accent on a pale page is
     how accents disappear. --swipe is the highlighter mark from the moodboard:
     at 26% it still reads olive, and ink laid over it still measures 10.9:1, so
     highlighting a line never costs that line its legibility. */
  /* A shade of the ink, not a fifth colour. Pure #000 would be a cold black
     against a warm near-black, and the seam shows on a pressed button. */
  --ink-deep: #0E0D0B;

  --olive-deep:  #4D582D;
  --olive-swipe: rgba(107, 122, 63, .26);
  --olive-soft:  rgba(107, 122, 63, .16);
  --olive-edge:  rgba(107, 122, 63, .55);

  --bg: var(--paper);
  /* Cards are the same tone as the page. That single decision is what lets both
     halves of the moodboard coexist. The editorial half builds structure out of
     hairline rules rather than boxes, so a card wants an edge, not a fill. The
     neumorphic half only works when an element and its background are the same
     colour — the instant they differ, the soft shadow stops reading as an
     embossed surface and becomes an ordinary drop shadow under a floating box. */
  --surface: var(--paper);
  --surface-2: var(--paper-lo); /* recessed: inputs, table heads, segmented tracks */

  /* Two weights of the one grey. Both were set against the *sunken* fill rather
     than the page, because that is the darker ground and table headings live on
     it — a grey chosen against the paper alone measures 4.25 there and fails.
     Set this way, both pass on every surface in the app: 6.0 and 5.1 on paper,
     5.4 and 4.6 on the sunken fill. */
  --muted: var(--grey);
  --faint: #656156;     /* smallest labels, table heads, separators */

  /* Structure. Two weights, and the difference between them is a rule about
     meaning rather than taste: --border is decoration, so it can be a whisper,
     but --border-strong is sometimes the only thing marking the edge of a
     control, and an edge that carries that job has to clear 3:1. It does, at
     3.43 — ink at 52%, not a fifth colour. */
  --border: rgba(26, 24, 21, .14);
  --border-strong: rgba(26, 24, 21, .52);

  /* Anything selected or pressed is filled with ink, not olive. This isn't a
     compromise — it is the moodboard: the nav in the reference sheet uses
     outlined pills with a solid black one for the active item. It also happens
     to be the only version that works, because paper on olive measures 3.9:1
     and ink on olive 3.78:1, so an olive pill cannot legibly hold its own
     label in either direction. Paper on ink is 14.73:1. */
  --accent: var(--ink);
  --accent-ink: var(--ink);
  --accent-soft: var(--paper-lo);
  --accent-wash: var(--olive-soft);

  /* Money in. Olive fills the shapes — bars, areas, dots, the mascot's ball —
     and ink sits beside them rather than on them. */
  --money: var(--olive);
  --money-soft: var(--olive-soft);
  --money-edge: var(--olive-edge);
  --money-deep: var(--olive-deep);

  /* Money out. With one chromatic colour on the whole site, in and out can't be
     told apart by hue, so they're told apart by weight: earned is a solid olive
     bar, spent is an ink bar at partial alpha. That reads at a glance and keeps
     working in greyscale, print, and for anyone red-green colourblind — which
     the usual green/red convention does not. */
  --spend: var(--ink);
  --spend-soft: rgba(26, 24, 21, .14);

  /* Anything that went wrong: ink, the loudest value available, plus an olive
     rule down the edge to mark it as different from ordinary emphasis. */
  --danger: var(--ink);
  --danger-soft: rgba(26, 24, 21, .08);
  /* .52 rather than .28, which measured 1.82:1 and made the delete button the one
     control in the app whose outline you could not reliably see. Matched to
     --border-strong so it clears the 3.0 bar that every other control edge does. */
  --danger-edge: rgba(26, 24, 21, .52);

  /* Owed, unpaid, worth a second look. The only place olive carries meaning
     rather than decoration, and it is always paired with a word — colour alone
     is never the signal. */
  --warn: var(--olive);
  --warn-soft: var(--olive-soft);
  --warn-edge: var(--olive-edge);

  /* Tighter than before. Editorial layout is built on straight lines meeting at
     corners; a generous radius softens exactly the thing the grid is for. */
  --radius: 10px;
  --radius-sm: 6px;

  /* Neumorphism, scaled down from the reference sheet in ./UI. The proportions
     there are for a large card — blur 19/20 at ±10 — and applying those to a
     32px button gives you a smudge, so the offsets and blur are roughly halved
     and the dark alpha comes down from 36% to 14%.

     The shadow colours are the ink and plain white rather than the reference's
     #2C2A33 and #0D2750, which would have been two more colours for no visible
     gain at these alphas.

     --nu-out is at rest, --nu-in is pressed or recessed. The swap between them
     is the entire interaction: the surface goes from standing proud of the page
     to sunk into it, which is what a physical button does. */
  --nu-out:
    -4px -4px 9px rgba(255, 255, 255, .72),
     4px  4px 9px rgba(26, 24, 21, .14);
  --nu-out-sm:
    -2px -2px 5px rgba(255, 255, 255, .66),
     2px  2px 5px rgba(26, 24, 21, .11);
  --nu-in:
    inset -3px -3px 7px rgba(255, 255, 255, .62),
    inset  3px  3px 7px rgba(26, 24, 21, .15);
  --nu-in-deep:
    inset -4px -4px 10px rgba(255, 255, 255, .55),
    inset  4px  4px 10px rgba(26, 24, 21, .18);

  /* Cards get the faintest possible version. They are not touchable, so they
     should not look pressable — this is just enough to lift the paper off the
     page without turning into a floating box. */
  --shadow:
    -1px -1px 2px rgba(255, 255, 255, .55),
     1px  2px 5px rgba(26, 24, 21, .055);

  /* Libre Bodoni for display, Public Sans for everything else — the skill's
     "Magazine Style" pairing. Bodoni is a didone: the thin strokes are very
     thin, which is what makes it feel like a printed masthead and also what
     makes it fall apart small, so there is a hard floor at 22px below which
     nothing uses it. Public Sans is a Franklin-derived grotesque with real
     tabular figures, which is the requirement a money app actually has —
     without them a column of dollar amounts doesn't line up. */
  --serif: 'Libre Bodoni', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --sans: 'Public Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font: var(--sans);
  --mono: "SF Mono", ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Paper tooth. Where the colour wash used to be — the wash existed to give the
   frosted cards something to refract, and there are no frosted cards now.

   What replaces it is texture rather than colour: a single tile of SVG noise,
   fixed so it behaves like stock the page is printed on rather than something
   the content drags around with it. Deliberately at the edge of perception. At
   3.5% you cannot see it on a blank area and you certainly cannot see it under
   type, but side by side with a flat fill the flat one looks like a screen and
   this one looks like paper. Any stronger and it starts fighting small text.

   It costs one 140-byte data URI and no repaint — it never animates, and being
   fixed it is composited once. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.app { display: flex; min-height: 100vh; }

/* ---------- Sidebar ---------- */
/* Same paper as the page, separated by a hairline. In the reference sheets the
   column rules do all the structural work and nothing is ever boxed in — the
   moment the sidebar takes a fill of its own it becomes a panel bolted to the
   side of a document, which is the opposite of the read we want. */
.sidebar {
  width: 248px;
  flex-shrink: 0;
  background: var(--paper);
  border-right: 1px solid var(--border-strong);
  padding: 22px 16px;
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
}

.brand {
  display: flex; align-items: center; gap: 12px;
  padding: 4px 8px 18px; margin-bottom: 14px;
  border-bottom: 1px solid var(--border-strong);
}
/* Ink, not olive. The mark used to be an olive tile with the letters knocked out
   of it, which measured 3.78:1 — under the 4.5 a 15px bold word needs, so the
   brand of the app was the least legible thing in the sidebar. Ink gives 14.73.

   The olive is still here, as the dot on the corner: a shape rather than a
   background, which is the only way this colour is allowed to be used.

   Left as initials rather than becoming the beetle. A logo sits on every screen,
   which is the definition of the thing the mascot was scoped away from — it is
   meant to turn up where there is something to say. Worth reopening as a
   deliberate choice, but not one to make silently inside a contrast pass. */
/* Same file as the tab and as the outside pages — see .out-mark for why there is
   only one drawing. Bigger here because the sidebar has the room.

   The olive dot that used to sit on the bottom-right corner is gone with it. It
   was there to hint at the beetle's ball against a mark that had no beetle in it;
   now the mark IS the beetle, and an olive dot stuck to an olive disc reads as a
   rendering fault rather than a flourish. */
.brand-mark {
  width: 40px; height: 40px; flex: none;
  display: block;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-family: var(--serif); font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
/* The bracketed, letterspaced micro-label that runs through every reference
   sheet. Caps at this size need the tracking — set solid they read as a smudge. */
.brand-text span {
  font-size: 9.5px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .16em; font-weight: 600;
  margin-top: 3px;
}

.nav { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
.nav-item {
  display: flex; align-items: center; gap: 11px;
  border: none; background: none; cursor: pointer;
  font-family: inherit; font-size: 14px; color: var(--muted);
  padding: 10px 12px; border-radius: var(--radius-sm);
  text-align: left; width: 100%; transition: background .12s, color .12s;
}
/* Line icons rather than dots. They inherit the row's colour through
   currentColor, so the active row's icon turns cream with its label and there is
   no second colour rule to keep in sync. Sized to the cap height of the label so
   the row still reads as a line of text with a mark beside it.

   stroke-width is in the icon's own coordinates, not screen pixels: a 24-unit
   drawing shown at 17px scales every stroke by 0.7, so 2 here lands at 1.4px on
   screen. Written as 1.6 it would render at 1.1 and go faint, which is the usual
   reason small line icons look washed out. */
.nav-item .nav-ico {
  width: 17px; height: 17px; flex: none;
  stroke: currentColor; fill: none;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  opacity: .82; transition: opacity .12s;
}
.nav-item:hover .nav-ico, .nav-item.active .nav-ico { opacity: 1; }
.nav-item:hover { background: var(--accent-wash); color: var(--ink); }
/* The page you're on is a pressed button, so it takes the dark blue. */
.nav-item.active { background: var(--accent); color: var(--bg); font-weight: 600; }
.nav-item.active:hover { background: var(--accent); color: var(--bg); }

/* A child of the item above it. The rule on the left does the work of saying
   "this belongs to that" without needing a second level of dots. */
.nav-sub {
  margin-left: 18px; padding-left: 14px; padding-top: 8px; padding-bottom: 8px;
  border-left: 1px solid var(--border); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: 13.5px; color: var(--faint);
}
.nav-sub:hover { border-left-color: var(--border-strong); }
.nav-sub.active { border-left-color: var(--accent); }

/* A nav row that owns a fold-out group. The chevron sits on top of the row's
   right edge rather than beside it, so the item underneath keeps its full-width
   hover and active highlight — the sidebar's rhythm doesn't break just because
   one row has an extra control. */
.nav-row { position: relative; display: flex; }
.nav-fold {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 22px; height: 22px; display: grid; place-items: center;
  border: none; background: none; border-radius: 6px;
  color: var(--faint); cursor: pointer; transition: background .12s, color .12s;
}
.nav-fold:hover { background: var(--accent-soft); color: var(--accent-ink); }
/* Pointing right means "folded away"; down means "the group below is open".
   Rotation hangs off the open state rather than the closed one, matching the
   collapse toggles on the cards — same direction of travel, same rule shape. */
.nav-fold .chev {
  font-size: 17px; line-height: 1; display: block;
  transition: transform .18s ease;
}
.nav-fold[aria-expanded="true"] .chev { transform: rotate(90deg); }
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-group[hidden] { display: none; }

.sidebar-foot {
  margin-top: auto; font-size: 11.5px; color: var(--faint);
  padding: 14px 8px 4px; line-height: 1.45;
  border-top: 1px solid var(--border);
}
/* The pill has to hold its own line here. It used to be inline-block and the
   sentence ran on after it; as an inline-flex box the sentence still runs on,
   but the wrapped remainder starts back at the container edge and reads as if it
   has fallen out of the sidebar's margin. Giving it a line of its own is the fix
   — and it is the better setting anyway, since it is a standing label about the
   app rather than the opening words of the sentence beneath it.

   This is an override and not the definition. `.pill` is defined once, down in
   the components section beside its tone modifiers. */
.sidebar-foot .pill { display: flex; width: fit-content; }
/* The signed-in block. The name is the loud line because it is the one that
   answers "whose books am I looking at" — the question that matters when you
   have two accounts open in two windows, which is exactly how this gets
   demoed. */
.acct-who { color: var(--ink); font-weight: 600; font-size: 12.5px; }
.acct-mail { font-size: 11px; color: var(--faint); word-break: break-all; margin-top: 1px; }
/* Name and address on the left, gear on the right. min-width:0 on the text half
   is what stops a long address from shoving the gear off the sidebar: a flex item
   defaults to min-width:auto and refuses to shrink below its content, and
   .acct-mail already breaks anywhere, so without this the row grows instead of
   the text wrapping. */
.acct-row { display: flex; align-items: center; gap: 8px; }
.acct-id { min-width: 0; flex: 1; }

/* The gear.
   Square, quiet at rest, and the same 17px drawing size as the nav icons so the
   two read as one family. It is the only control in the sidebar that is an icon
   with no label, which is affordable for exactly one reason: a gear in a corner
   is the most learned symbol in software, and the title and aria-label carry the
   word for anyone the picture does not reach.

   Active takes the same ink fill as an active nav row, because it is the same
   state — you are on that page — and inventing a second visual language for it
   would say the page is a different kind of thing than it is. */
.acct-gear {
  flex: none; width: 30px; height: 30px;
  display: grid; place-items: center;
  border: none; background: none; border-radius: var(--radius-sm);
  color: var(--faint); cursor: pointer;
  transition: background .12s, color .12s;
}
.acct-gear svg {
  width: 17px; height: 17px;
  stroke: currentColor; fill: none;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}
.acct-gear:hover { background: var(--accent-wash); color: var(--ink); }
.acct-gear.active { background: var(--accent); color: var(--bg); }
.acct-gear.active:hover { background: var(--accent); color: var(--bg); }
.acct-gear:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }

/* ---------- Main ---------- */
/* min-width:0 is doing real work here. A flex item defaults to min-width:auto,
   which refuses to shrink below its own content — so between roughly 900px and
   1400px the column stayed at its 1120px max-width, shoved past the right edge
   and put a horizontal scrollbar under the whole app. Wide tables already scroll
   inside .table-wrap, so the column itself is free to narrow. */
/* overflow-x:clip, not hidden. `hidden` would turn this into a scroll container,
   which changes what the sticky sidebar sticks to; `clip` just trims and leaves
   overflow-y visible, so nothing else moves. It is here because .hint::after is
   an absolutely positioned bubble centred on its icon — a hint near the right
   edge reached ~15px past the page and put a scrollbar under the whole app even
   while the bubble was invisible, since out-of-flow boxes still count toward
   scrollable overflow whatever their opacity. */
.main { flex: 1; min-width: 0; overflow-x: clip; padding: 34px 40px 80px; max-width: 1120px; }
.view { display: none; animation: fade .18s ease; }
.view.active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* The page title, and the one piece of structure that makes the app feel like a
   printed thing rather than a screen: a serif title with a hairline ruled all
   the way across underneath it. That rule is doing the same job the rules do in
   every reference sheet — it is the horizon the content hangs from. */
.view-head {
  margin-bottom: 28px; padding-bottom: 16px;
  border-bottom: 1px solid var(--border-strong);
}
.view-head h1 {
  font-family: var(--serif);
  margin: 0 0 6px; font-size: 31px; font-weight: 600; letter-spacing: -.015em;
  line-height: 1.15;
}
.view-head p { margin: 0; color: var(--muted); font-size: 13.5px; max-width: 62ch; }

/* Section headings stay sans and small. The serif is a display face here, not a
   hierarchy — using it at every level would make the page shout in six sizes at
   once, which is the opposite of editorial. Below the 22px floor Bodoni's thin
   strokes break up anyway, so the floor and the taste point the same way. */
h2 {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .14em; color: var(--muted);
}
h3 { font-size: 15px; font-weight: 700; letter-spacing: -.005em; color: var(--ink); }

/* The tracked micro-label from the reference sheets, available anywhere. */
.eyebrow {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .18em; color: var(--faint);
}

/* The page plate. Every reference sheet frames its page with two tiny bracketed
   labels in the top corners rather than a header bar — [ WILDNUN STUDIO ] on the
   left, [ EYE CREAM ] on the right. It is a printer's device, and it does two
   jobs at once: it says what the sheet is and which part of it you are holding,
   while costing the page almost nothing.

   Here the left plate never changes and the right one names the section. That
   second half is the useful one, because on a drill-down — a single song, three
   levels in — the sidebar still highlights "Projects" and nothing on the page
   tells you which branch of the app you wandered into. The plate does. */
.plate {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  margin-bottom: 20px;
}
.plate-mark {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .18em; color: var(--faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The brackets are drawn, not typed, so they can never be selected or read out
   as text — they are a frame, not a word. The non-breaking space inside each one
   lets the .18em tracking set the gap instead of a hand-picked margin. */
.plate-mark::before { content: "[\00a0"; }
.plate-mark::after { content: "\00a0]"; }
/* The highlighter mark. Sits behind the text at 26%, which keeps ink over it at
   10.9:1 — a highlight that costs the sentence nothing. */
.swipe {
  background: linear-gradient(var(--olive-swipe), var(--olive-swipe));
  background-size: 100% 62%; background-position: 0 88%; background-repeat: no-repeat;
  padding: 0 2px;
}

/* ---------- The beetle ---------- */
/* The mascot is line art drawn in currentColor, so it takes the ink of whatever
   it is standing in and never needs a palette of its own. The one exception is
   the ball, which is olive — the ball is the money, and the money is the pop of
   colour. That is the whole colour logic of the character.

   Line weights are in the drawing's own units, so they scale with it. The three
   weights are a hierarchy: legs heaviest because they carry the pose and have to
   survive at 90px wide, shell outline next, surface detail lightest. */
.bug { display: block; color: var(--ink); }
.bug-fill {
  fill: var(--bug-paper, var(--bg));
  stroke: currentColor; stroke-width: 2; stroke-linejoin: round;
}
.bug-line { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.bug-leg { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.bug-ball { fill: var(--olive); stroke: currentColor; stroke-width: 2; }
/* Coins packed into the ball. Ink on olive is 3.78:1 — under the bar for text,
   fine for texture, and dropped to a third of that so it reads as grain rather
   than as a second drawing competing with the beetle. */
.bug-coin { fill: none; stroke: var(--ink); stroke-width: 1.4; opacity: .32; }
.bug-eye { fill: var(--bug-paper, var(--bg)); stroke: currentColor; stroke-width: 1.7; }
.bug-pupil { fill: currentColor; stroke: none; }
.bug-spark { fill: var(--bug-paper, var(--bg)); stroke: none; }
/* The hairline the beetle stands on — the same rule that structures every other
   page in the reference sheets, doing double duty here as ground. */
.bug-ground { fill: none; stroke: currentColor; stroke-width: 1.2; opacity: .38; }

/* ---------- Buttons ---------- */
/* This is where the neumorphism lives. The rule agreed before building was "on
   things you touch only", and a button is the most touched thing here.

   At rest the surface stands proud of the page; pressed, it sinks into it. That
   swap — --nu-out to --nu-in — is the whole interaction, and it is the one worth
   having, because it is what a physical button does and it needs no colour, no
   size change and no movement of the layout under your finger.

   The hairline border is not decoration. Neumorphism's known failure is that a
   control defined purely by soft shadow disappears the moment those shadows are
   flattened — forced-colours mode, a high-contrast setting, a cheap panel viewed
   off-axis. The border is at 3.43:1, which clears the 3:1 a control edge needs
   on its own, so the button survives with the shadows gone entirely.

   Rank is carried by fill, the way it is on a printed page: solid for the one
   action that matters, embossed paper for everything else. */
.btn {
  font-family: inherit; font-size: 13.5px; font-weight: 600;
  letter-spacing: .01em;
  padding: 10px 17px; border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--border-strong);
  box-shadow: var(--nu-out-sm);
  cursor: pointer;
  transition: background .16s, color .16s, border-color .16s, box-shadow .16s;
}
.btn:hover { background: var(--paper-hi); box-shadow: var(--nu-out); }
.btn:active { background: var(--paper-lo); box-shadow: var(--nu-in); }

/* The one loud action per screen. Solid ink, paper type, 14.73:1 — and no
   emboss, because an embossed surface has to be the same colour as the page for
   the illusion to hold, and this one deliberately isn't. It gets a plain drop
   instead, and still sinks on press. */
.btn.primary {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
  box-shadow: 0 2px 5px rgba(26,24,21,.16);
}
.btn.primary:hover { background: var(--ink-deep); border-color: var(--ink-deep); box-shadow: 0 3px 9px rgba(26,24,21,.22); }
.btn.primary:active { background: var(--ink); box-shadow: inset 0 2px 5px rgba(0,0,0,.34); }

/* Confirm. Ink like the primary, because olive cannot hold a label — 3.78:1 with
   ink on it, 3.90 with paper — so a filled olive button would be the least
   readable control in the app. The olive is the bar down the leading edge
   instead: a shape, the only thing this colour is allowed to be. */
.btn.success {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
  box-shadow: 0 2px 5px rgba(26,24,21,.16);
  border-left: 4px solid var(--olive);
}
.btn.success:hover { background: var(--ink-deep); border-color: var(--ink-deep); border-left-color: var(--olive); box-shadow: 0 3px 9px rgba(26,24,21,.22); }
.btn.success:active { background: var(--ink); border-left-color: var(--olive); box-shadow: inset 0 2px 5px rgba(0,0,0,.34); }

/* Tertiary: no emboss at all, no edge until you reach for it. The quietest thing
   that is still unmistakably a button. */
.btn.ghost {
  background: transparent; color: var(--muted);
  border-color: transparent; box-shadow: none;
}
.btn.ghost:hover { background: var(--surface); color: var(--ink); border-color: var(--border-strong); box-shadow: var(--nu-out-sm); }
.btn.ghost:active { background: var(--paper-lo); color: var(--ink); border-color: var(--border-strong); box-shadow: var(--nu-in); }

.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
.btn:disabled:hover, .btn:disabled:active { background: var(--surface); box-shadow: none; }

/* A button that is doing something.

   The import button had no such state, and that is the whole reason this rule
   exists: it sat there looking inert for the several seconds an import takes,
   so it got clicked three times and the statement imported three times.

   Olive, and a ring — a shape, which is the only thing this colour is ever
   allowed to be. It reads at 3.78:1 on the ink of a primary button and 3.9:1 on
   paper, and being the one chromatic value in the app it cannot be mistaken for
   anything else on the page.

   The disabled dimming is deliberately cancelled. "Working" and "unavailable"
   are different messages and must not look the same: a busy button greyed to 45%
   says the app has given up, when what is true is the opposite. It still cannot
   be clicked — `disabled` is set and `cursor: progress` says so — it simply does
   not look abandoned while it can't be. */
.btn.is-working { cursor: progress; }
.btn:disabled.is-working { opacity: 1; }
.btn.is-working::before {
  content: ""; display: inline-block; vertical-align: -2px;
  width: 13px; height: 13px; margin-right: 9px;
  border: 2px solid var(--olive); border-top-color: transparent;
  border-radius: 50%;
  animation: iw-working .72s linear infinite;
}
@keyframes iw-working { to { transform: rotate(360deg); } }
/* Olive, and one of the few places it carries a job rather than decorating. It
   measures 3.9:1 on the paper and 3.78 on an ink button — both over the 3:1 a
   focus indicator needs — and being the only chromatic value on the site it is
   impossible to mistake for anything else on the page. */
.btn:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }
.btn.tiny { padding: 5px 11px; font-size: 12px; }
/* A download is a real link, not a button that fakes one — it should still look
   like the rest of the toolbar, and it should not carry link underlines. */
a.btn { display: inline-block; text-decoration: none; white-space: nowrap; }

/* ---------- Cards & fields ---------- */
/* Paper on paper. No fill of its own, a hairline edge, and the faintest possible
   emboss — the card should read as a panel scored into the sheet rather than a
   tile resting on top of it. The padding is up from 22 to 26 because the whole
   editorial read depends on negative space: hairlines only work as structure
   when there is enough room around them for the eye to see them as structure and
   not as clutter. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px; margin-bottom: 20px;
  box-shadow: var(--shadow);
}
.step-tag {
  display: inline-block; font-size: 11.5px; font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--accent-ink); background: var(--accent-soft);
  padding: 4px 10px; border-radius: 20px; margin-bottom: 16px;
}
.field { margin-bottom: 15px; }
.field:last-child { margin-bottom: 0; }
.field label {
  display: block; font-size: 10px; font-weight: 700; color: var(--faint);
  text-transform: uppercase; letter-spacing: .14em; margin-bottom: 7px;
}
.field label .opt { font-weight: 600; color: var(--faint); text-transform: none; letter-spacing: 0; }
.field-row { display: flex; gap: 14px; }
.field-row .field { flex: 1; }

/* Inputs are the inset half of the neumorphism: a field is a slot cut into the
   sheet, so it is pressed in at rest rather than raised. It is also the honest
   affordance — a raised input looks like a button, which is the single most
   common thing neumorphic interfaces get wrong.

   The hairline is at 3.43:1 and carries the boundary on its own, so the field is
   still findable with the shadows stripped out. */
input[type="text"], input[type="date"], input[type="month"], input[type="search"],
input[type="number"], input[type="email"], input[type="password"],
select, textarea {
  width: 100%; font-family: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 10px 12px;
  box-shadow: var(--nu-in);
  transition: border-color .16s, box-shadow .16s, background .16s;
}
/* Focus deepens the recess and rings it in olive. Olive on the paper is 3.9:1,
   over the 3:1 a focus indicator needs, and since it is the only chromatic value
   in the app there is nothing it can be confused with. */
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--olive);
  background: var(--paper-lo);
  box-shadow: var(--nu-in-deep), 0 0 0 3px var(--olive-soft);
}
input::placeholder, textarea::placeholder { color: var(--faint); opacity: 1; }
textarea { resize: vertical; }
input[type="file"] { padding: 8px; background: var(--surface-2); }

/* ---------- Dashboard ---------- */
/* The masthead. Flat ink now rather than a gradient into a second colour: with
   one chromatic value on the site there is no second colour to run to, and a
   gradient between two shades of the same near-black is a gradient nobody can
   see. Flat is also the more honest read for an editorial page — it is a block
   of solid ink on paper, which is exactly what a masthead is. */
.hero {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--ink);
  color: var(--paper); border-radius: var(--radius); padding: 30px 32px; margin-bottom: 24px;
  box-shadow: 0 4px 16px rgba(26,24,21,.14);
}
.hero-total { display: flex; flex-direction: column; }
.hero-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: .18em;
  font-weight: 700; color: var(--paper-lo);
}
/* The largest thing on the site, so it is the one that most has to be the
   serif — this is the number the whole app exists to show. Bodoni's thin strokes
   are an asset at 46px and a liability under 22, which is why the floor exists. */
.hero-value {
  font-family: var(--serif);
  font-size: 46px; font-weight: 600; letter-spacing: -.02em;
  margin: 10px 0 6px; font-variant-numeric: tabular-nums; line-height: 1;
}
.hero-sub { font-size: 12.5px; color: var(--paper-lo); }
/* Ink is already the fill here, so a ghost button can't get quieter by losing
   its own — it lifts out of the panel instead. Both states clear 4.5:1 against
   the ink underneath. */
.hero .btn.ghost { background: rgba(237,234,225,.10); color: var(--paper); border-color: rgba(237,234,225,.34); box-shadow: none; }
.hero .btn.ghost:hover { background: rgba(237,234,225,.20); color: var(--paper); border-color: rgba(237,234,225,.55); box-shadow: none; }
.hero .btn.ghost:active { background: var(--paper); color: var(--ink); border-color: var(--paper); box-shadow: none; }
.hero .btn:focus-visible { outline-color: var(--olive); }

.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow); }
.panel h2 { margin: 0 0 16px; font-size: 14px; color: var(--muted); font-weight: 600; }

.bars { display: flex; flex-direction: column; gap: 12px; }
.bar-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; align-items: baseline; }
.bar-label { font-size: 13.5px; font-weight: 500; }
.bar-amount { font-size: 13.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.bar-count { font-size: 11.5px; color: var(--faint); }
.bar-track { grid-column: 1 / -1; height: 7px; background: var(--surface-2); border-radius: 5px; overflow: hidden; border: 1px solid var(--border); }
/* Money in, so green — and green the whole way. The gradient runs deep-to-plain
   within the one hue rather than blue-to-green: a bar that starts blue reads as
   two different things joined in the middle. */
.bar-fill { height: 100%; background: linear-gradient(90deg, var(--money-deep), var(--money)); border-radius: 5px; min-width: 3px; transition: width .4s ease; }
.bar-empty { color: var(--faint); font-size: 13px; font-style: italic; }

/* What the uploaded file says it is. The olive rule down the side does the
   signalling — the text itself stays ink, per the palette rule that the green
   marks shapes and never glyphs. The confirming case is deliberately quieter
   than the correcting one: agreeing with you is not news. */
.src-detect { margin-top: 10px; font-size: 13px; line-height: 1.5; padding: 9px 12px; border-radius: var(--radius-sm); }
.src-detect.ok { color: var(--muted); background: var(--surface-2); border-left: 3px solid var(--olive-edge); }
.src-detect.warn { color: var(--ink); background: var(--olive-soft); border-left: 3px solid var(--olive); }
.src-detect strong { font-weight: 650; }

/* A button that reads as a sentence rather than a control. Used where the offer
   to overrule the app has to be present without competing with the statement
   it's attached to. */
.linkish {
  background: none; border: 0; padding: 0; font: inherit; color: var(--ink);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.linkish:hover { color: var(--olive-deep); }

/* ---------- Import: the explainer ---------- */
/* The one part of this screen aimed at somebody who has never done this. It gets
   the beetle and the top of the page; once outgrown it collapses to the single
   toggle line and stops standing between them and the upload box. */
.explain { display: flex; flex-direction: column; }
.explain.folded { padding-top: 14px; padding-bottom: 14px; }
.explain-toggle {
  align-self: flex-start; background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--muted);
  text-decoration: underline; text-underline-offset: 3px;
}
.explain-toggle:hover { color: var(--ink); }
.explain:not(.folded) .explain-toggle { margin-bottom: 18px; }
.explain-body { display: grid; grid-template-columns: 132px 1fr; gap: 24px; align-items: start; }
/* The rule that makes "Hide this" hide anything. It was missing entirely.
   Everything else worked — the button toggled .folded, relabelled itself and
   updated aria-expanded, and the padding rule above even shrank the card — but
   nothing ever told the body to go away, so the guide stayed on screen and the
   button read as broken. It was missing on all four guide pages at once, which
   is what a purely-CSS gap looks like: the behaviour is identical everywhere
   because the behaviour is nowhere. */
.explain.folded .explain-body { display: none; }
.explain-art { padding-top: 4px; }
.explain-text h2 {
  font-family: var(--serif); font-size: 21px; font-weight: 500; letter-spacing: 0;
  text-transform: none; color: var(--ink); margin: 0 0 8px;
}
.explain-lede { font-size: 14.5px; line-height: 1.55; color: var(--ink); margin: 0 0 14px; }
.explain-steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 9px; }
.explain-steps li { font-size: 13.5px; line-height: 1.55; color: var(--muted); }
.explain-steps strong { color: var(--ink); font-weight: 650; }
.explain-safe {
  margin: 16px 0 0; padding-left: 12px; border-left: 3px solid var(--olive-edge);
  font-size: 13px; line-height: 1.5; color: var(--muted);
}
@media (max-width: 720px) {
  .explain-body { grid-template-columns: 1fr; }
  .explain-art { display: none; }
}

/* ---------- Import: the file ---------- */
/* The file is the whole of step one now, so it gets the room the source dropdown
   used to take and reads as the one thing being asked for. */
.dropzone {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 34px 20px; text-align: center;
  border: 1.5px dashed var(--olive-edge); border-radius: var(--radius-sm);
  background: var(--surface-2); transition: background .12s ease, border-color .12s ease;
}
.dropzone.dragging { background: var(--olive-soft); border-color: var(--olive); }
/* The real input covers the whole zone at zero opacity rather than being hidden,
   so it stays keyboard-reachable and the entire area is clickable. */
.dropzone input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer;
}
.drop-cta {
  position: relative; z-index: 1; pointer-events: none;
  display: inline-block; padding: 11px 22px; border-radius: var(--radius-sm);
  background: var(--ink); color: var(--paper); font-size: 14px; font-weight: 600;
}
.dropzone:hover .drop-cta { background: var(--ink-deep); }
.drop-hint { margin: 0; font-size: 13px; color: var(--muted); }

.file-card {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding: 10px 12px;
  background: var(--surface-2); border-radius: var(--radius-sm);
  border-left: 3px solid var(--olive);
}
.file-name { font-weight: 600; font-size: 13.5px; overflow-wrap: anywhere; }
.file-size { font-size: 12px; color: var(--faint); font-family: var(--mono); }
.file-card .linkish { margin-left: auto; font-size: 13px; color: var(--muted); }

/* Pasting is the fallback for the one person in fifty with rows on a clipboard
   and no file, so it folds away rather than sitting alongside as an equal. */
.paste-fold { margin-top: 16px; }
.paste-fold > summary {
  cursor: pointer; font-size: 13px; color: var(--muted); list-style: none;
  display: inline-flex; align-items: center; gap: 6px;
}
.paste-fold > summary::-webkit-details-marker { display: none; }
.paste-fold > summary::before { content: "+"; font-family: var(--mono); color: var(--olive); }
.paste-fold[open] > summary::before { content: "\2212"; }
.paste-fold > summary:hover { color: var(--ink); }
.paste-fold textarea { margin-top: 12px; }
.paste-fold .btn { margin-top: 10px; }
/* A button *after* the fold rather than inside it — the catalog importer's one
   action, which reads the textarea and the dropzone alike, so it belongs to
   both and sits below both. Without this it renders flush against the "…or
   paste the rows instead" line, close enough to look like part of it. Same
   16px as the fold's own top margin, so the card keeps one rhythm. */
.paste-fold + .btn { margin-top: 16px; }

/* ---------- Import: asking who sent it ---------- */
/* Only ever seen when every detection strategy came up empty. Styled as a
   question rather than a warning: nothing has gone wrong, the app simply hasn't
   met this layout before and is about to learn it. */
.src-ask {
  margin-top: 14px; padding: 16px;
  background: var(--olive-soft); border-left: 3px solid var(--olive);
  border-radius: var(--radius-sm);
}
.src-ask-q { margin: 0 0 4px; font-size: 14.5px; font-weight: 650; color: var(--ink); }
.src-ask-sub { margin: 0 0 14px; font-size: 13px; line-height: 1.5; color: var(--muted); }
.src-ask-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.src-ask-chips .chip {
  padding: 6px 12px; font-size: 13px; font-family: var(--font); cursor: pointer;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--border); border-radius: 999px;
}
.src-ask-chips .chip:hover { border-color: var(--olive); background: var(--paper-hi); }
.src-ask-chips .chip.on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.src-ask-row { display: flex; gap: 10px; flex-wrap: wrap; }
.src-ask-input { flex: 1 1 220px; }

/* ---------- Import mapping ---------- */
.map-note { font-size: 13px; color: var(--muted); margin: 0 0 16px; }
.map-note.saved { color: var(--accent-ink); }
.map-grid { display: flex; flex-direction: column; gap: 12px; margin-bottom: 18px; }
.map-row {
  display: grid; grid-template-columns: 130px 120px 1fr; gap: 12px; align-items: center;
  padding: 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.map-row .map-field-name { font-weight: 600; font-size: 13.5px; }
.map-row .map-field-name small { display: block; font-weight: 400; color: var(--faint); font-size: 11px; }
.map-sample { font-size: 12px; color: var(--muted); font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-sample em { color: var(--faint); font-family: var(--font); }

/* The amount row's working-out, tucked directly beneath it. Sits inside the
   same flex column as the map rows, so a negative top margin and a squared-off
   top edge are what join it to the row above rather than any nesting. */
.amt-evidence {
  margin-top: -8px; padding: 12px 14px;
  border: 1px solid var(--border); border-top: none;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  background: var(--surface-2); font-size: 13px;
}
.amt-evidence.ok { background: var(--money-soft); border-color: var(--money-edge); }
.amt-evidence.warn { background: var(--warn-soft); border-color: var(--warn-edge); }
.amt-evidence.bad { background: var(--danger-soft); border-color: var(--danger-edge); }
.amt-verdict { margin: 0; line-height: 1.5; }
.amt-evidence.ok .amt-verdict { color: var(--ink); }
.amt-evidence.warn .amt-verdict { color: var(--warn); }
.amt-evidence.bad .amt-verdict { color: var(--danger); }
.amt-evidence .btn { margin-top: 10px; }
.amt-why { margin-top: 10px; }
.amt-why summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
.amt-why summary:hover { color: var(--ink); }
.amt-head { margin: 12px 0 6px; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; }
.amt-list { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--muted); line-height: 1.7; }
.amt-list code { font-family: var(--mono); font-size: 12px; color: var(--ink); }
.amt-list strong { color: var(--ink); font-weight: 600; }

.preview-summary { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
/* Chips carry the same vocabulary as .pill, for the same reason: olive cannot
   hold a label in either direction — ink on olive is 3.78:1 and olive on the
   wash is 3.25:1, both under the 4.5 bar. So the label always stays ink on
   paper and the STATE is carried by the outline plus a small olive mark:
   filled dot = good, hollow ring = needs attention. Shape does the work, so it
   still reads in greyscale and to a colourblind eye. */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 600; padding: 6px 12px; border-radius: 20px;
  background: transparent; color: var(--ink); border: 1px solid var(--border-strong);
}
.chip.good { border-color: var(--olive-edge); }
.chip.good::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--olive); flex: none; }
.chip.warn { border-color: var(--olive-edge); }
.chip.warn::before { content: ""; width: 6px; height: 6px; border-radius: 50%; border: 1.5px solid var(--olive); flex: none; }
.chip.info { background: var(--surface-2); color: var(--ink); border-color: var(--border); }

/* Inline "assign untagged songs to an artist" panel in the import preview. */
.unmapped-panel { background: var(--warn-soft); border: 1px solid var(--warn-edge); border-radius: var(--radius-sm); padding: 16px; margin-bottom: 16px; }
.unmapped-head { font-size: 13.5px; margin-bottom: 12px; line-height: 1.5; }
.unmapped-head strong { color: var(--warn); }
.unmapped-rows { display: flex; flex-direction: column; gap: 8px; max-height: 340px; overflow-y: auto; }
.unmapped-row { display: flex; align-items: center; gap: 12px; }
.unmapped-song { flex: 1; font-weight: 600; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.unmapped-amt { width: 88px; text-align: right; color: var(--ink); font-weight: 600; font-size: 13px; flex: none; }
/* Descendant selector needed to out-specify the global input[type=text] width:100%. */
.unmapped-panel .unmapped-artist { width: 220px; flex: none; margin: 0; }
#imp-assign { margin-top: 14px; }

/* Overlap warning. Louder than the untagged-songs panel on purpose: an untagged
   song is untidy, an imported statement line counted twice is wrong. */
.overlap-panel {
  background: var(--danger-soft); border: 1px solid var(--danger-edge);
  border-radius: var(--radius-sm); padding: 16px; margin-bottom: 16px;
}
/* Same panel, no alarm: this file touches a month that already has money in it,
   and not one of its rows has ever been imported. That is the ordinary case for
   any distributor whose stores report at different speeds — Apple a month
   behind, YouTube and Tidal nearer two — and it used to be drawn in the same
   red as a genuine duplicate. Which is how somebody ended up re-mapping their
   date column to make the red box go away, and lost $0.76 doing it. A panel
   that shouts at correct behaviour teaches people to route around it. */
.overlap-panel.calm { background: var(--surface-2); border-color: var(--border); }
.overlap-panel.calm .overlap-table { border-color: var(--border); }
.overlap-head { display: flex; gap: 11px; align-items: flex-start; margin-bottom: 13px; }
.overlap-icon {
  flex: none; width: 21px; height: 21px; margin-top: 1px; border-radius: 50%;
  background: var(--danger); color: var(--bg); font-size: 13px; font-weight: 800;
  display: grid; place-items: center;
}
.overlap-icon.info { background: var(--muted); font-style: italic; }
.overlap-head strong { font-size: 14px; }
.overlap-sub { margin: 3px 0 0; font-size: 13px; color: var(--muted); line-height: 1.55; }
.overlap-panel .overlap-table {
  background: var(--surface); border: 1px solid var(--danger-edge); border-radius: var(--radius-sm);
  overflow: hidden; margin-bottom: 14px; font-size: 13px;
}
.overlap-panel .overlap-table th { position: static; background: transparent; }
.overlap-panel .overlap-table td { padding: 8px 14px; }
.overlap-panel .overlap-table tbody tr:hover { background: transparent; }

.overlap-choice { display: flex; gap: 10px; flex-wrap: wrap; }
.overlap-opt {
  flex: 1 1 220px; text-align: left; font-family: inherit; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 11px 13px 12px; display: flex; flex-direction: column; gap: 3px;
  transition: border-color .12s, box-shadow .12s, background .12s;
}
.overlap-opt strong { font-size: 13.5px; color: var(--ink); }
.overlap-opt span { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.overlap-opt:hover { border-color: var(--border-strong); }
.overlap-opt.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: var(--accent-soft); }
.overlap-opt.on strong { color: var(--accent-ink); }
.overlap-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.overlap-effect { margin: 12px 0 0; font-size: 12.5px; color: var(--muted); line-height: 1.55; }

/* The last gate before anything is written. Set off from the card with a rule
   and its own background so the question doesn't read as one more field to
   scroll past — this is the moment the file becomes your books. */
.confirm-block {
  margin-top: 20px; padding: 18px 18px 16px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.confirm-q { margin: 0; font-size: 16px; font-weight: 600; color: var(--ink); }
.confirm-sub { margin: 5px 0 0; font-size: 13px; color: var(--muted); line-height: 1.55; }
.confirm-sub strong { color: var(--ink); font-weight: 600; }
.confirm-sub code { font-family: var(--mono); font-size: 12.5px; color: var(--ink); }
.confirm-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }

/* Folded away by default. Most imports don't need it, and a check that's
   always in the way is a check people learn to click past. */
.confirm-check { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
.confirm-check.hidden { display: none; }
.confirm-check summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
.confirm-check summary:hover { color: var(--ink); }
.check-how { margin: 10px 0 12px; font-size: 12.5px; color: var(--muted); line-height: 1.6; }
.check-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.check-field { display: flex; flex-direction: column; gap: 5px; }
.check-field span { font-size: 12px; font-weight: 600; color: var(--muted); }
/* Descendant selectors to out-specify the global input[type=text] width:100%. */
.check-field select, .check-row .check-field input[type="text"] { width: 168px; margin: 0; }
.check-result { margin: 12px 0 0; font-size: 13px; line-height: 1.6; padding: 11px 13px; border-radius: var(--radius-sm); }
.check-result.match { background: var(--money-soft); border: 1px solid var(--money-edge); color: var(--ink); }
.check-result.match strong { color: var(--ink); }
/* Only the match panel goes flex, because only the match panel has a beetle in
   it. A mismatch gets plain prose and no mascot — the drawing is there to mark
   the good outcome, and a character that shows up for bad news too is just a
   border with a face on it. */
.check-result.match { display: flex; align-items: center; gap: 12px; }
.check-result.match .bug { flex: none; margin: -6px 0; }
@media (max-width: 560px) { .check-result.match .bug { display: none; } }
.check-result.miss { background: var(--warn-soft); border: 1px solid var(--warn-edge); color: var(--ink); }
.check-result.miss strong { color: var(--warn); }
.check-result .btn { margin-top: 10px; }

/* Where "No, something's off" lands you. The select is already focused; this
   just makes it findable if the page had to scroll to get there. */
.map-column.attention { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* ---------- Tables ---------- */
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
/* Tables stay flat and crisp. The neumorphism was scoped to things you touch,
   and a table is a thing you read — soft shadows behind dense small type is how
   data becomes unreadable. Structure here comes from hairlines, which is also
   how every reference sheet in the folder does it. */
.data-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* Tracked caps, and considerably tighter than before: 10px with .16em of
   tracking rather than 11.5 with .4px. Small caps set solid read as a smudge —
   the letterspacing is what makes them legible at this size, not the size. The
   rule under the row is heavier than the rules between rows, because a heading
   that is only distinguished by colour disappears the moment the table scrolls. */
.data-table th {
  text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .16em;
  color: var(--faint); font-weight: 700; padding: 13px 14px;
  border-bottom: 1px solid var(--border-strong);
  background: var(--surface-2); position: sticky; top: 0;
}
/* Sortable column heads. The whole cell is the click target — an arrow glyph on
   its own is too fiddly to aim at — and the arrow keeps its slot whether or not
   it's showing, so the headings don't shuffle sideways as you click between
   columns. Hover is a colour change only: these tables sit flush inside cards
   where a filled hover state would read as a second row of chrome. */
.data-table th.sortable { cursor: pointer; user-select: none; transition: color .12s; }
.data-table th.sortable:hover { color: var(--accent-ink); }
.data-table th.sorted { color: var(--accent-ink); }
.th-label { display: inline-flex; align-items: center; gap: 4px; }
.th-arrow { display: inline-block; width: 8px; font-size: 10px; line-height: 1; }

.data-table td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--surface-2); }
.data-table .num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.data-table .date { white-space: nowrap; color: var(--muted); }
.data-table .nowrap { white-space: nowrap; }
/* Every figure is ink, negatives included. Nothing is lost by that: fmtMoney
   already writes the sign into the string, so the minus is doing the work and
   the colour was only ever repeating it. Repeating it in colour *alone* is the
   thing WCAG 1.4.1 exists to stop, so the table is better off this way — and it
   still reads correctly printed, photocopied, or by anyone colourblind. */
.data-table .money { color: var(--ink); }
.data-table .money.neg { color: var(--ink); }
.data-table .invalid { background: var(--danger-soft); }
.data-table .invalid:hover { background: var(--danger-soft); }
/* A preview row that will be left out because its month is already on record.
   Faded rather than hidden — you should be able to see what you're not taking. */
.data-table tr.dupe { opacity: .42; }
.data-table tr.dupe td:not(.muted) { text-decoration: line-through; text-decoration-thickness: 1px; }

.tag {
  display: inline-block; font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 20px;
  background: var(--accent-soft); color: var(--accent-ink); white-space: nowrap;
}
.tag.role { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
/* Locked. Legible but not alarming — the row is correct, it is the plan that is
   limited, and a red tag on money somebody is owed reads as "this is wrong". */
.tag.warn { background: transparent; color: var(--warn); border: 1px solid var(--warn); }

/* The whole row dimmed, on the same argument as tr.dupe just above: you should
   be able to see what is not being counted, which is the entire reason these
   rows are still listed. Amounts keep full contrast — a locked statement's
   figure is the one thing on this page somebody is likely to want to read. */
.data-table tr.is-locked td { opacity: .62; }
.data-table tr.is-locked td.money { opacity: 1; }
.warn-text { color: var(--warn); font-size: 12px; }
.muted { color: var(--faint); }

.icon-btn {
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--muted);
  width: 28px; height: 28px; border-radius: 7px; cursor: pointer; font-size: 15px; line-height: 1;
  display: inline-grid; place-items: center; transition: all .12s;
}
/* The default icon button is a delete. Without a red in the palette, weight does
   the warning instead of hue: destructive fills solid, an edit pencil only
   tints. Same colour, two very different amounts of it. */
.icon-btn:hover { background: var(--danger); color: var(--bg); border-color: var(--danger); }
.icon-btn.edit:hover { background: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent-soft); }

.table-foot { display: flex; justify-content: space-between; padding: 12px 4px; font-size: 13px; color: var(--muted); }
.table-foot strong { color: var(--ink); font-variant-numeric: tabular-nums; }

/* ---------- Filter bar ---------- */
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.filter-bar select, .filter-bar input { width: auto; min-width: 140px; flex: 0 0 auto; }
.filter-bar input[type="search"] { flex: 1; min-width: 180px; }

/* ---------- Empty ---------- */
.empty {
  text-align: center; padding: 40px 20px; color: var(--muted);
  background: var(--surface); border: 1px dashed var(--border-strong); border-radius: var(--radius); margin-bottom: 18px;
}
.empty a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
.empty a:hover { text-decoration: underline; }
.empty .bug { margin: 0 auto 14px; }

/* A story moment: the beetle, and one line saying what happened. Centred and
   given room, because the value of a moment is that it is the only thing there —
   crowd it with buttons and stats and it becomes a header.

   The line is capped at about 52 characters. Wider than that and the eye has to
   hunt for the start of the next line on centred text, which is the one thing
   centred text is genuinely bad at. */
.bug-note {
  display: flex; flex-direction: column; align-items: center;
  gap: 6px; padding: 22px 20px 26px; text-align: center;
}
.bug-note p { margin: 0; max-width: 52ch; font-size: 13.5px; color: var(--muted); }

.hidden { display: none !important; }

/* And the attribute, which the browser only hides by a rule any `display` in
   this file outranks.

   `[hidden] { display: none }` comes from the user-agent stylesheet, and every
   author rule beats every UA rule regardless of specificity. So the moment an
   element with a `display` of its own is hidden by setting the attribute — which
   is what el() and every `node.hidden = true` in app.js do — it stays on screen.

   Found on the dashboard of a brand-new account: the note saying what a total
   covers is hidden for anybody not at their upload limit, which is nearly
   everybody, and `.coverage-note { display: flex }` kept it drawn. Not as text —
   the text is empty when it is hidden — but as a 48-pixel olive bar across the
   top of the first screen anybody sees, containing one unexplained button.

   This is the third time. `.nav-group[hidden]` (line 362) and
   `.collapsible .collapse-body[hidden]` (2197) are the same bug, found twice
   before and patched where they were found. Both are now redundant and harmless;
   they stay because deleting them proves nothing and this rule covers them.

   `!important` to match `.hidden` above, and for the same reason: an element told
   to be hidden is hidden, and there is no arrangement of later rules where the
   right answer is to argue with that. */
[hidden] { display: none !important; }

/* ---------- Toasts ---------- */
.toast-wrap { position: fixed; bottom: 22px; right: 22px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
/* Every toast keeps the ink ground — it is the only fill that holds body text at
   full contrast (14.73:1). Olive on ink measures 3.78:1, which fails, so the
   variants are told apart by MARK and EDGE rather than by background colour.
   That also means they still read in greyscale, in print, and to a colourblind
   eye — colour is never the only signal (WCAG 1.4.1). */
.toast {
  background: var(--ink); color: var(--bg); padding: 12px 16px; border-radius: var(--radius-sm);
  font-size: 13.5px; box-shadow: 0 10px 30px rgba(26,24,21,.3); max-width: 340px;
  animation: toastIn .2s ease; display: flex; align-items: center; gap: 10px;
  border-left: 4px solid transparent;
}
.toast::before { content: "●"; font-size: 9px; opacity: .8; flex: none; }
.toast.success { border-left-color: var(--olive); }
.toast.success::before { content: "●"; color: var(--olive); opacity: 1; }
.toast.error { border-left-color: var(--paper); }
.toast.error::before { content: "▲"; color: var(--paper); opacity: 1; font-size: 8px; }
@keyframes toastIn { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: none; } }

/* ---------- Artist detail ---------- */
/* Dashboard by-artist bars become buttons that drill into the artist page.
   The box-shadow spread paints a padded highlight without shifting layout. */
.bar-row.clickable { cursor: pointer; border-radius: 8px; transition: background .12s, box-shadow .12s; }
.bar-row.clickable:hover { background: var(--accent-soft); box-shadow: 0 0 0 6px var(--accent-soft); }
.bar-row.clickable:hover .bar-label { color: var(--accent-ink); }
.bar-row.clickable:hover .bar-label::after { content: " ›"; font-weight: 700; }
.bar-row.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.artist-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.artist-head h1 { margin: 0; }
.artist-head #artist-back { flex: none; }
.artist-head #artist-sub { flex-basis: 100%; margin: 0; color: var(--muted); font-size: 14px; }
.artist-hero { margin-bottom: 20px; }
.artist-panels { grid-template-columns: 1fr; margin-bottom: 20px; }
#artist-album-note { margin-bottom: 18px; }

/* Album sort / expand toolbar. */
.album-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.album-toolbar-spacer { flex: 1; }
.album-sort { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
.album-sort select { width: auto; padding: 6px 10px; font-size: 13px; }

.album-section { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 14px; overflow: hidden; }
.album-section:last-of-type { margin-bottom: 0; }
/* The whole header is the collapse toggle. */
.album-head {
  display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none;
  padding: 12px 16px; background: var(--surface-2); border-bottom: 1px solid var(--border);
}
.album-section:not(.open) .album-head { border-bottom: none; }
.album-head:hover { background: var(--surface); }
.album-caret { flex: none; color: var(--muted); font-size: 11px; transition: transform .15s ease; }
.album-section.open .album-caret { transform: rotate(90deg); }
.album-name { font-weight: 700; font-size: 14.5px; }
/* Two classes out-specify the global input[type=text] width:100% so this stays a
   narrow inline field sitting next to the album name. */
.album-head .album-year {
  width: 62px; flex: none; margin: 0; padding: 5px 8px; font-size: 12.5px; text-align: center;
}
.album-count { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; margin-left: auto; white-space: nowrap; }
.album-amt { flex: none; color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
/* Collapsed by default; the .open class reveals the songs. */
.album-songs { display: none; flex-direction: column; }
.album-section.open .album-songs { display: flex; }

.song-row {
  display: grid; grid-template-columns: minmax(140px, 1.5fr) minmax(110px, 1fr) 84px 190px;
  gap: 12px; align-items: center; padding: 10px 16px; border-bottom: 1px solid var(--border);
}
.song-row:last-child { border-bottom: none; }
.song-name { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.song-sources { display: flex; flex-wrap: wrap; gap: 6px; }
.src-chip {
  font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 20px;
  background: var(--accent-soft); color: var(--accent-ink); white-space: nowrap; font-variant-numeric: tabular-nums;
}
.song-amt { text-align: right; color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
/* Descendant selector out-specifies the global input[type=text] width:100%. */
.artist-albums .album-assign { width: 190px; flex: none; margin: 0; font-size: 13px; padding: 7px 10px; }
.artist-albums .album-assign:disabled { background: var(--surface); color: var(--faint); cursor: not-allowed; }
#artist-save-albums { margin-top: 16px; }

/* Spotify album suggestions. The two extra-class selectors beat the global
   input[type=text] rule so the highlight actually paints. */
.spotify-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: -6px 0 16px; }
.spotify-status { font-size: 12.5px; line-height: 1.4; }
.spotify-status.ok { color: var(--accent-ink); font-weight: 600; }
.spotify-status.err { color: var(--danger); }

/* The manual-verification prompt. Each row is one proposal the app wouldn't
   make on its own, with the reason underneath — the reason is the whole reason
   the row is here, so it gets its own line rather than a tooltip. */
/* Wider than the standard dialog: each row is "long song title → long album
   title" on one line, and at 540px the album wraps under the arrow and the row
   stops reading as a sentence. */
#review-dialog { width: min(680px, calc(100vw - 32px)); }
#review-list { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 8px; max-height: 46vh; overflow-y: auto; }
.review-row {
  display: grid; grid-template-columns: auto 1fr; gap: 11px; align-items: start;
  padding: 11px 12px; border-radius: var(--radius-sm); cursor: pointer;
  border: 1px solid transparent;
}
.review-row:hover { background: var(--surface-2); border-color: var(--border); }
.review-row input { margin-top: 3px; flex: none; }
.review-body { display: grid; grid-template-columns: auto auto 1fr; gap: 7px; align-items: baseline; }
.review-song { font-weight: 600; font-size: 13.5px; }
.review-arrow { color: var(--faint); }
.review-album { font-size: 13.5px; }
.review-artist { color: var(--faint); font-size: 11.5px; }
.review-reason { grid-column: 1 / -1; color: var(--muted); font-size: 11.5px; line-height: 1.45; margin-top: 3px; }

/* ---------- First run ----------
   The only screen in the app with no numbers on it, so it carries the beetle and
   a single instruction. Narrow measure on purpose: this is the one place someone
   reads prose rather than scanning figures. */
.firstrun { max-width: 560px; margin: 8px auto 0; padding: 32px 28px 30px; text-align: center; }
.firstrun .bug { display: block; margin: 0 auto 10px; }
/* Overrides the app-wide section-label treatment on h2 (uppercase, tracked out,
   grey). That styling is right for "INCOME BY SOURCE" sitting above a chart and
   wrong for the one sentence a new user reads first — this is a headline, not a
   label, so it gets ink and normal case. */
.firstrun h2 {
  font-family: var(--serif); font-size: 30px; margin: 0 0 12px;
  text-transform: none; letter-spacing: -.01em; color: var(--ink); font-weight: 600;
}
.firstrun-lede { font-size: 15.5px; line-height: 1.55; margin: 0 0 14px; }
.firstrun-why { color: var(--muted); font-size: 13.5px; line-height: 1.6; margin: 0 0 22px; }
.firstrun-steps {
  list-style: none; padding: 0; margin: 0 0 26px; text-align: left;
  display: flex; flex-direction: column; gap: 11px;
}
.firstrun-steps li {
  font-size: 13.5px; line-height: 1.55; color: var(--muted);
  padding-left: 15px; border-left: 2px solid var(--olive-edge);
}
.firstrun-steps strong { color: var(--ink); font-weight: 650; }
.firstrun-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* An unticked row reads as declined rather than merely unread. */
.review-row:has(input:not(:checked)) .review-song,
.review-row:has(input:not(:checked)) .review-album { color: var(--faint); text-decoration: line-through; }
.album-assign.suggested { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); font-weight: 600; }
.album-assign.review { border-color: var(--warn); background: var(--warn-soft); }

/* ---------- PRO claiming ---------- */
/* Status bar: last cross-check + catalog info + actions. */
.pro-statusbar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 16px 20px; margin-bottom: 18px; box-shadow: var(--shadow);
}
.pro-status-main { display: flex; flex-direction: column; gap: 2px; }
.pro-status-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--faint); font-weight: 600; }
.pro-status-value { font-size: 16px; font-weight: 700; }
.pro-status-sub { font-size: 12.5px; color: var(--muted); }
.pro-status-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* A small labelled dropdown that changes what the page is showing — which PRO
   the tracker is pointed at, which year a breakdown covers. Sits with the
   actions rather than in the page header, because it belongs next to the other
   controls that change what you're looking at. */
.pro-picker, .year-picker { display: flex; align-items: center; gap: 7px; }
.pro-picker > span, .year-picker > span {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); font-weight: 600;
}
.pro-picker select, .year-picker select {
  font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--ink);
  padding: 5px 9px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: var(--surface); cursor: pointer;
}
.pro-picker select:hover, .year-picker select:hover { border-color: var(--faint); }

/* A closing line under a page, for provenance rather than content — where these
   numbers came from, and where to go if you want the raw rows. */
.fine-print { font-size: 12px; color: var(--muted); line-height: 1.6; margin: 4px 2px 18px; }
.fine-print a { color: var(--accent-ink); }

/* The three piles reuse the album-section collapse mechanics; these tune the head
   tint per pile and lay out each song row. */
/* --money-soft and --warn-soft both resolve to the same olive now, so tint alone
   can no longer tell the claimed pile from the unclaimed one. The left edge does
   it instead: solid olive = claimed, dashed olive = still owed to you, no bar =
   not yours. Names stay ink throughout; olive on these tints is 3.2:1. */
.pro-section .album-count { margin-left: auto; }
.pro-claimed .album-head { background: var(--olive-soft); border-left: 4px solid var(--olive); }
.pro-claimed .album-head:hover { background: var(--olive-swipe); }
.pro-unclaimed .album-head { background: var(--paper-lo); border-left: 4px dashed var(--olive); }
.pro-unclaimed .album-head:hover { background: var(--paper-deep); }
.pro-notmine .album-head { background: var(--surface-2); border-left: 4px solid transparent; }
.pro-claimed .album-name,
.pro-unclaimed .album-name { color: var(--ink); }
/* These three heads sit on the bare page rather than inside a card, so their
   tints composite over the paper and land darker than the same tint would on a
   card — neither grey clears 4.5 on any of them, and the hovers are darker
   still. The caret and the count take ink here and let size and weight carry the
   hierarchy instead of colour. */
.pro-section .album-head .album-caret,
.pro-section .album-head .album-count { color: var(--ink); }

.pro-row {
  display: grid; grid-template-columns: minmax(150px, 1.3fr) minmax(150px, 2fr) auto;
  gap: 12px; align-items: center; padding: 11px 16px; border-bottom: 1px solid var(--border);
}
.pro-row:last-child { border-bottom: none; }
.pro-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pro-song { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Straight on the cream, where the faint grey is 4.3:1 — one of the few labels
   in the app that isn't sitting on a card. */
.pro-artist { font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pro-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
.pro-workid { font-size: 11px; font-family: var(--mono); color: var(--muted); }
.pro-writers {
  font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 100%;
}
.pro-badge {
  font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 20px; white-space: nowrap;
  background: var(--surface-2); color: var(--muted); border: 1px solid var(--border);
}
.pro-badge.ok { background: var(--money); color: var(--ink); border-color: transparent; }
.pro-badge.pending { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-edge); }
.pro-badge.muted { background: var(--accent-soft); color: var(--accent-ink); border-color: transparent; }

/* A near-miss suggestion: the similar catalog title + a Link button. */
.pro-suggest {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 4px 3px 10px;
  background: var(--accent-soft); border-radius: 20px; max-width: 100%;
}
.pro-suggest-label {
  font-size: 11.5px; font-weight: 600; color: var(--accent-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
}
.pro-suggest .btn.tiny { padding: 3px 9px; background: var(--surface); border-color: var(--border-strong); }

.pro-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.pro-actions .btn.tiny { padding: 5px 9px; font-size: 12px; }
/* Two classes out-specify the global input[type=text] width:100%. */
.pro-row .pro-note { width: 100%; margin: 0; font-size: 13px; padding: 7px 10px; }
.pro-empty { padding: 18px 16px; color: var(--faint); font-size: 13px; font-style: italic; }

/* ---------- Education ---------- */
.nav-sep { height: 1px; background: var(--border); margin: 10px 8px; }

.edu-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(244px, 1fr)); gap: 16px; }
.edu-card {
  text-align: left; font: inherit; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 20px; box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: 9px;
  transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease;
}
button.edu-card:hover { transform: translateY(-2px); border-color: var(--accent); }
.edu-card h3 { margin: 3px 0 0; font-size: 16px; color: var(--ink); letter-spacing: -.2px; }
.edu-card p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.5; flex: 1; }
.edu-ic {
  width: 44px; height: 44px; border-radius: 12px; flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
}
.edu-ic svg { width: 24px; height: 24px; }
.edu-go { color: var(--accent-ink); font-weight: 600; font-size: 13px; }
.edu-card.soon { cursor: default; box-shadow: none; opacity: .74; }
.edu-card.soon:hover { transform: none; border-color: var(--border); }
.edu-card.soon .edu-ic { background: var(--surface-2); color: var(--faint); }
.edu-card.soon h3 { color: var(--muted); }

.edu-article-head { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.edu-block { margin-bottom: 18px; }
.edu-h { margin: 0 0 8px; font-size: 18px; letter-spacing: -.2px; }
.edu-h3 { margin: 0 0 10px; font-size: 15px; }
.edu-lead { font-size: 16px; line-height: 1.6; margin: 0 0 8px; }
.edu-note { color: var(--muted); font-size: 14px; line-height: 1.6; margin: 8px 0; }
.edu-note strong, .edu-lead strong { color: var(--ink); }
.edu-cap { color: var(--faint); font-size: 12.5px; line-height: 1.55; margin: 12px 0 0; }
.edu-cap strong { color: var(--muted); }
.hl-accent { color: var(--accent-ink); font-weight: 600; }
.edu-list { margin: 12px 0 0; padding-left: 20px; display: flex; flex-direction: column; gap: 9px; color: var(--muted); font-size: 14px; line-height: 1.55; }
.edu-list strong { color: var(--ink); }
.edu-list.tight { gap: 6px; font-size: 13px; margin-top: 4px; }
.edu-list.check { list-style: none; padding-left: 0; }
.edu-list.check li { position: relative; padding-left: 27px; }
.edu-list.check li::before { content: "\2713"; position: absolute; left: 3px; top: -1px; color: var(--accent); font-weight: 700; }
.edu-cta { margin-top: 16px; padding-top: 15px; border-top: 1px solid var(--border); }
.edu-cta .edu-cap { margin: 0 0 4px; }
.edu-btn-row { display: flex; flex-wrap: wrap; gap: 10px; margin: 4px 0 2px; }
.edu-btn-row .btn { flex: 1 1 220px; }
.edu-two { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.edu-two .edu-block { margin-bottom: 0; }
.edu-callout.warn { border-color: var(--warn-edge); background: linear-gradient(180deg, var(--warn-soft), var(--surface) 70%); }
.edu-callout.accent { border-color: var(--accent-soft); background: linear-gradient(180deg, var(--accent-wash), var(--surface) 70%); }

/* Inline SVG diagrams — themed via the app's CSS variables. */
.dg { width: 100%; height: auto; display: block; margin: 4px 0 2px; }
.dg text { font-family: var(--font); }
.dg-box { fill: var(--surface-2); stroke: var(--border-strong); stroke-width: 1.5; }
.dg-box.accent { fill: var(--accent-soft); stroke: none; }
.dg-box.money { fill: var(--money-soft); stroke: none; }
.dg-box.warn { fill: var(--warn-soft); stroke: none; }
.dg-box.hl { stroke: var(--accent); stroke-width: 2.5; }
.dg-t { fill: var(--ink); font-weight: 700; }
.dg-t.accent { fill: var(--accent-ink); }
.dg-t.money { fill: var(--ink); }
.dg-t.warn { fill: var(--warn); }
.dg-s { fill: var(--muted); }
.dg-cap { fill: var(--faint); }
.dg-line { stroke: var(--border-strong); stroke-width: 2; fill: none; }
.dg-line.dash { stroke-dasharray: 4 4; }
.dg-arrowhead { fill: var(--faint); }
.dg-circle { fill: var(--accent); }
.dg-num { fill: var(--bg); font-weight: 700; }
.dg-pill { fill: var(--accent-soft); }
.dg-link { fill: transparent; pointer-events: all; cursor: pointer; }
.dg-link:hover, .dg-link:focus-visible { stroke: var(--accent); stroke-width: 2.5; }
.dg-link:focus { outline: none; }

/* ---------- Checklists ---------- */
/* The card runs edge-to-edge so item rows can span its full width; each block
   supplies its own horizontal padding. */
.ck-card { padding: 0; overflow: hidden; }
.ck-head { padding: 22px 22px 18px; border-bottom: 1px solid var(--border); }
.ck-head h2 { margin: 0 0 6px; font-size: 18px; letter-spacing: -0.2px; }
.ck-head p { margin: 0; max-width: 64ch; color: var(--muted); font-size: 13.5px; line-height: 1.55; }

/* No tint behind the hill. It had one, and the drawing is capped at 680px inside a
   strip that runs the full width of the card, so the tint showed up as two empty
   tan margins with a picture floating between them — a panel that had not been
   filled. The hairline underneath does the separating on its own, which is what
   every sheet in the reference folder uses instead of a box. */
.ck-progress { padding: 20px 22px 16px; border-bottom: 1px solid var(--border); }

/* ---------- The hill ---------- */
/* Capped, and centred in the strip. The drawing is 1000×300 in its own units, so
   width only controls how much of the page it eats — the slope of the hill lives
   in the viewBox and cannot be flattened by a wide screen. Left uncapped it would
   be 300px tall on a desktop, which is more than a progress indicator has earned. */
.ck-climb { position: relative; max-width: 680px; margin: 0 auto; }
.climb { display: block; width: 100%; height: auto; }
/* Solid olive below the crest, open paper above it. Not a style choice — it is
   what keeps the beetle's money-ball visible. On paper the ball reads instantly;
   on an olive ground it disappears, which is what the first two attempts did. */
.climb-hill { fill: var(--olive); }
/* Stroked 30 wide and centred on the crest, so 15 falls above the line where it
   is paper on paper and vanishes, and 15 falls below where it cuts a pale road
   into the hillside. One stroke, two jobs. */
.climb-road { fill: none; stroke: var(--paper); stroke-width: 30; }
.climb-edge { fill: none; stroke: var(--ink); stroke-width: 1.4; opacity: .42; }
/* The crest: the ground the beetle stands on and the horizon the ball is read
   against. The only line in the drawing carrying information rather than form. */
.climb-crest { fill: none; stroke: var(--ink); stroke-width: 2; }
/* Hollow ring = still to do, solid disc = done. Two different shapes, not two
   different colours, so the state survives greyscale and colour blindness. */
.climb-stop { fill: var(--paper); stroke: var(--ink); stroke-width: 2.6; }
.climb-stop.done { fill: var(--ink); }
/* A phase's first item: a wider bead, no numeral. The radius carries the whole
   difference, which is all it has to carry — the phase numbers are printed in the
   list below and repeating them up here put three numbered stations in a huddle
   at the bottom of the road. Nothing reads only from this, so a size change with
   no second signal is honest here in a way it would not be for done/not-done. */
.climb-stop.phase { stroke-width: 3; }
/* Big numeral, tiny letterspaced label under it — the device every sheet in the
   reference folder uses to fill a large empty area without crowding it.

   Laid over the drawing rather than drawn inside it, so these are real pixels at
   every width instead of viewBox units that shrink with the picture. Placed in
   percentages against the top-left corner, which is open sky whatever the hill is
   doing: the road leaves from the bottom-left and climbs away to the right. */
.climb-score {
  position: absolute; left: 3.5%; top: 8%; margin: 0; pointer-events: none;
  font-family: var(--serif); font-size: 42px; font-weight: 600;
  line-height: 1; letter-spacing: -.01em; color: var(--ink);
}
.climb-score span {
  display: block; margin-top: 5px;
  font-family: var(--sans); font-size: 11px; font-weight: 700;
  letter-spacing: .2em; color: var(--faint);
}
/* The drawing keeps its proportions all the way down, so past a point the hill is
   96px tall and a 42px numeral standing in front of it is no longer a caption. */
@media (max-width: 700px) {
  .climb-score { font-size: 28px; }
  .climb-score span { font-size: 9px; margin-top: 3px; }
}

.ck-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
/* The score is drawn inside the hill now. This keeps the same fact in the DOM for
   a screen reader — the drawing is aria-hidden and announces nothing — without
   printing it on the page twice. Clipped rather than display:none, because a
   display:none element is not read out either. */
.ck-count {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.ck-tools { display: flex; gap: 8px; margin-left: auto; flex-wrap: wrap; }

.ck-phase { display: flex; align-items: flex-start; gap: 12px; padding: 22px 22px 12px; }
.ck-phase-n {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--accent); color: var(--bg); font-size: 13px; font-weight: 700;
}
.ck-phase h3 { margin: 2px 0 3px; font-size: 15.5px; }
.ck-phase p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.5; }

.ck-item { border-top: 1px solid var(--border); }
/* The first item after a phase header sits flush under it. */
.ck-phase + .ck-item { border-top: none; }
.ck-row { display: flex; align-items: center; gap: 12px; padding: 13px 22px; }
.ck-row:hover { background: var(--surface-2); }
.ck-box { flex: none; width: 19px; height: 19px; margin: 0; accent-color: var(--accent); cursor: pointer; }
.ck-title { font-size: 14.5px; font-weight: 600; line-height: 1.4; cursor: pointer; }
.ck-item.done .ck-title { text-decoration: line-through; color: var(--faint); font-weight: 500; }

/* All four tags are outlined rather than filled, and the difference between them
   is the weight of the edge and whether there's a dot — not the fill.

   The two that changed had to. `.every` was ink on solid olive at 3.78:1 and
   `.ongoing` was olive text on a pale olive wash at 3.25:1, both under the 4.5
   an 11px label needs. They were the only two places left in the app asking the
   accent to carry a word, and it cannot: 3.9:1 against the paper is its ceiling.

   Outlining moves the olive to the edge, where 3.9 is comfortably over the 3:1
   a boundary needs, and puts every label back on ink at 13.3:1 or better. */
.ck-tag {
  flex: none; margin-left: auto; padding: 3px 10px; border-radius: 20px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .12em; white-space: nowrap;
  text-transform: uppercase;
  display: inline-flex; align-items: center; gap: 5px;
  background: transparent; color: var(--ink);
  border: 1px solid var(--border-strong);
}
/* One-off tasks: quietest, no mark. */
.ck-tag.once { color: var(--muted); border-color: var(--border); }
/* Recurring work is the thing worth spotting on this page, so it gets the dot. */
.ck-tag.every { color: var(--ink); border-color: var(--olive-edge); }
.ck-tag.every::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--olive); flex: none;
}
.ck-tag.opt { color: var(--muted); border-color: var(--border); border-style: dashed; }
/* Ongoing: same dot, hollow — running rather than repeating. */
.ck-tag.ongoing { color: var(--ink); border-color: var(--olive-edge); }
.ck-tag.ongoing::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  border: 1.5px solid var(--olive); flex: none;
}

.ck-caret {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; border: 1px solid var(--border); border-radius: 7px;
  background: var(--surface); color: var(--muted); cursor: pointer;
  transition: transform .15s ease, color .15s ease, border-color .15s ease;
}
.ck-caret svg { width: 15px; height: 15px; }
.ck-caret:hover { color: var(--accent); border-color: var(--accent); }
.ck-item.open .ck-caret { transform: rotate(90deg); color: var(--accent); border-color: var(--accent); }

/* Collapsed by default; .open reveals the breakdown. Left padding lines the
   detail up with the item title (22 padding + 19 checkbox + 12 gap). */
.ck-detail { display: none; padding: 2px 22px 20px 53px; }
.ck-item.open .ck-detail { display: block; }
.ck-why { margin: 0 0 12px; font-size: 13.5px; line-height: 1.6; }
.ck-steps { margin: 0 0 12px; padding-left: 18px; display: flex; flex-direction: column; gap: 7px; }
.ck-steps li { color: var(--muted); font-size: 13.5px; line-height: 1.55; }
.ck-steps li strong { color: var(--ink); }
.ck-note {
  margin: 0; padding: 10px 13px; border-radius: var(--radius-sm);
  background: var(--warn-soft); font-size: 13px; line-height: 1.55;
}
/* Cross-links size to their text here rather than stretching like they do on
   the education sheets. */
.ck-detail .edu-btn-row { margin-top: 14px; }
.ck-detail .edu-btn-row .btn { flex: 0 1 auto; }

@media (max-width: 620px) {
  .ck-head, .ck-progress, .ck-phase, .ck-row { padding-left: 16px; padding-right: 16px; }
  .ck-detail { padding-left: 16px; padding-right: 16px; }
  /* Grid rather than flex-wrap so the caret stays pinned to the right edge and
     the tag always drops to its own line, whatever the title length. */
  .ck-row {
    display: grid; align-items: center; gap: 7px 12px;
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "box title caret" ". tag tag";
  }
  .ck-box { grid-area: box; }
  .ck-title { grid-area: title; min-width: 0; }
  .ck-caret { grid-area: caret; }
  .ck-tag { grid-area: tag; justify-self: start; margin-left: 0; }
}

/* ---------- Finance dashboard ---------- */
/* A head-row is a title block on the left and an action on the right. Used in
   view headers and inside cards, so the h2/h1 margins have to survive both. */
.head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.head-row > div { min-width: 0; }
/* Sits on the page rather than in a card, so it takes the darker of the two greys. */
.asof { color: var(--muted); font-size: 12.5px; }

.card h2 { margin: 0 0 4px; font-size: 15.5px; letter-spacing: -0.2px; }
.card h2.h2-sm { font-size: 13.5px; color: var(--muted); }
.card-sub { margin: 0 0 16px; font-size: 12.5px; color: var(--muted); line-height: 1.55; max-width: 70ch; }
.head-row .card-sub { margin-bottom: 0; }
.card > .head-row { margin-bottom: 16px; }

.back-link { margin-bottom: 14px; }

/* Sub-tabs sit inside a view, under the h1 — a lighter weight than the sidebar nav. */
.subtabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
.subtab {
  font-family: inherit; font-size: 13.5px; font-weight: 600; color: var(--muted);
  background: none; border: none; border-bottom: 2px solid transparent;
  padding: 9px 14px; margin-bottom: -1px; cursor: pointer; transition: color .12s, border-color .12s;
  /* Never let a tab label wrap — the strip scrolls sideways on narrow screens. */
  white-space: nowrap; flex: none;
}
.subtab:hover { color: var(--ink); }
.subtab.active { color: var(--accent-ink); border-bottom-color: var(--accent); }
.dash-pane { display: none; }
.dash-pane.active { display: block; animation: fade .18s ease; }

/* Segmented filter control. */
.seg { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 3px; gap: 2px; }
.seg-btn {
  font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--muted);
  background: none; border: none; border-radius: 6px; padding: 6px 11px; cursor: pointer; transition: all .12s; white-space: nowrap;
}
.seg-btn:hover { background: var(--accent-wash); color: var(--ink); }
/* The segment that's on is a pressed button, so it takes the dark blue too. */
.seg-btn.active, .seg-btn.active:hover { background: var(--accent); color: var(--bg); }

.filter-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 16px; }
.mini-select { width: auto; min-width: 110px; font-size: 12.5px; padding: 7px 10px; }

/* ---------- Stat cards ---------- */
/* Four across is the usual row, but the count is set by whatever renderStats was
   given, so a row of three fills the width instead of leaving a gap. */
.stat-grid { display: grid; grid-template-columns: repeat(var(--cards, 4), 1fr); gap: 14px; margin-bottom: 18px; }
/* Raised. Stat tiles are one of the four things the neumorphism was scoped to,
   and they earn it — most of them are clickable, and the ones that aren't still
   read as the objects on the page you're meant to look at first. Same paper as
   everything else, so the emboss reads as the sheet pushed up from behind rather
   than a card dropped on top. The hairline is not decoration: it is what keeps
   the tile visible if the shadows are flattened by a high-contrast mode, forced
   colours, or simply a bad screen at an angle. */
.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
  box-shadow: var(--nu-out);
}
.stat-label {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .15em; color: var(--faint);
}
/* The display moment. Bodoni at 30px is the one place the serif gets to be loud,
   and money is the right thing to be loud about in a royalty app.

   tabular-nums is set on principle but does nothing here — Libre Bodoni ships no
   tnum table. It doesn't need to: this is a single figure with nothing to line
   up against. The columns that *do* need to line up are in tables, and those
   stay in Public Sans, which does have the table. */
.stat-value {
  font-family: var(--serif);
  font-size: 30px; font-weight: 600; letter-spacing: -.015em;
  margin: 8px 0 4px; font-variant-numeric: tabular-nums;
  line-height: 1.08;
}
/* Holds its line even when empty. Plenty of cards now have nothing to say under
   the number — the definition moved into the hint — and without this the card
   shrinks and sits short next to the ones beside it. */
.stat-sub { font-size: 12px; color: var(--muted); line-height: 1.4; min-height: 17px; }
/* The number itself stays ink — a 30px figure in olive is 3.9:1, which clears
   the large-text bar but sits below every other number on the page, so the one
   you most want read would be the faintest. The tile is flagged along its top
   edge instead: a shape, which is what this colour is for. */
.stat.good { border-top: 3px solid var(--money); }
.stat.bad { border-top: 3px solid var(--border-strong); }
/* The one filled tile: solid ink, paper type. 14.73:1 on the value, and the
   label and subtitle sit at 11.5:1 on the same ground. */
.stat.accent {
  background: var(--ink);
  border-color: var(--ink);
  box-shadow: var(--nu-out);
}
.stat.accent .stat-label { color: var(--paper-lo); }
.stat.accent .stat-value { color: var(--paper); }
.stat.accent .stat-sub { color: var(--paper-lo); }
/* The pop, once per screen. An olive hairline under the filled tile's number —
   the highlighter mark from the reference sheets, reduced to its thinnest
   possible form so it marks the figure without ever sitting behind a word. */
.stat.accent .stat-value::after {
  content: ""; display: block; width: 34px; height: 2px;
  background: var(--olive); margin-top: 10px;
}

/* A stat that opens its own breakdown. It's a <button>, so everything the base
   .stat rule assumes about a div has to be restated — text alignment, font,
   full width. The chevron only appears on hover so the resting dashboard stays
   as quiet as it was before these became clickable. */
.stat-link {
  position: relative; display: block; width: 100%; text-align: left;
  font: inherit; color: inherit; cursor: pointer;
  transition: transform .12s, box-shadow .12s, border-color .12s;
}
.stat-link:hover { transform: translateY(-2px); border-color: var(--border-strong); box-shadow: 0 10px 22px rgba(26,24,21,.12); }
.stat-link.accent:hover { border-color: transparent; box-shadow: 0 12px 30px rgba(26,24,21,.26); }
.stat-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.stat-go {
  position: absolute; top: 14px; right: 15px;
  font-size: 19px; line-height: 1; font-weight: 700; color: var(--faint);
  opacity: 0; transform: translateX(-3px); transition: opacity .12s, transform .12s;
}
.stat-link:hover .stat-go, .stat-link:focus-visible .stat-go { opacity: 1; transform: none; }
.stat-link.accent .stat-go { color: var(--accent-soft); }

/* The one piece of copy under a form field that has to stay visible: you need it
   while you're deciding what to type, not after. Named apart from .hint on
   purpose — that's the circled marker below, and the two are nothing alike. */
.field-note { display: block; margin-top: 5px; font-size: 11.5px; color: var(--faint); line-height: 1.4; }

/* Definitions live in here rather than under the number. "Net" doesn't need
   "everything it earned minus everything it cost" printed beneath it forever —
   you read that once. The line under a number is worth more when it's saved for
   something only this release can tell you, like the month it broke even.

   Deliberately a plain span, never a button: several of these sit inside
   .stat-link buttons, where a nested interactive element is invalid markup and
   swallows the click meant for the card. Standing on its own it takes focus
   directly; inside a card the card's own keyboard focus reveals it. */
/* Quietened with a colour, not with opacity. It used to rest at opacity .45 on
   whatever it inherited, which measured 1.88:1 — a control you cannot see until
   you happen to hover the four pixels it occupies. Opacity could not be tuned out
   of that either: the colours it inherits top out at 5.1:1 at full strength, so
   there is no fraction of them that clears 4.5.

   So the glyph gets its own colour at full strength (5.72:1) and the ring gets a
   separate, lighter one. Splitting them is the trick — the ring is a shape, judged
   against the 3.0 bar, and at .52 ink it measures 3.4, which keeps the marker
   looking like a faint circle while the "?" inside it is properly readable. */
.hint {
  display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; margin-left: 5px; vertical-align: 1px;
  border: 1px solid var(--border-strong); border-radius: 50%;
  color: var(--muted);
  font-size: 9px; font-weight: 700; line-height: 1;
  cursor: help; user-select: none; position: relative;
}
/* Drawn below the marker, not above. Every stat row in the app is the first
   thing under a page heading, so a bubble drawn upward would open into the
   header. Downward it opens over the card's own number, which is fine — it's
   floating with a shadow and you're reading the bubble, not the number. */
.hint::after {
  content: attr(data-hint);
  position: absolute; top: calc(100% + 7px); left: 50%;
  transform: translateX(-50%) translateY(-3px);
  width: max-content; max-width: 220px;
  padding: 7px 10px; border-radius: var(--radius-sm);
  background: var(--ink); color: var(--bg);
  font-size: 11.5px; font-weight: 500; line-height: 1.35; letter-spacing: 0;
  text-transform: none; text-align: left;
  box-shadow: 0 6px 18px rgba(26,24,21,.3);
  opacity: 0; pointer-events: none;
  transition: opacity .13s, transform .13s;
  z-index: 40;
}
/* :has(:focus-visible) rather than :focus-within, so a card that was clicked
   with a mouse doesn't sit there holding its bubble open afterwards. */
.hint:hover, .hint:focus-visible, .stat:has(:focus-visible) .hint { color: var(--ink); border-color: var(--ink); }
.hint:hover::after, .hint:focus-visible::after, .stat:has(:focus-visible) .hint::after { opacity: 1; transform: translateX(-50%); }
.hint:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* On the one dark card the whole thing inverts, hover included — the rule above
   would send it to ink on ink. */
.stat.accent .hint { color: var(--accent-soft); border-color: rgba(237, 234, 225, .55); }
.stat.accent .hint:hover, .stat.accent .hint:focus-visible, .stat.accent:has(:focus-visible) .hint {
  color: var(--paper-hi); border-color: var(--paper-hi);
}
/* Beside a heading rather than a stat label, so it scales up to match. */
h2 .hint { width: 15px; height: 15px; font-size: 10px; margin-left: 6px; vertical-align: 2px; }
h1 .hint { width: 18px; height: 18px; font-size: 11px; margin-left: 9px; vertical-align: 5px; }
h1 .hint::after { max-width: 300px; }

/* A heading whose explanation moved into a hint has no paragraph under it any
   more, and its own 4px is nowhere near enough air before a chart. This gives it
   the spacing the paragraph used to provide, so cards read the same either way. */
.card > h2:not(:has(+ .card-sub)) { margin-bottom: 14px; }
.head-row h2:not(:has(+ .card-sub)) { margin-bottom: 0; }
/* A heading is a single line now, so it centres against the button beside it
   instead of hanging off the top the way a heading-plus-paragraph block did.
   Any paragraph at all opts out: a two-line block wants the control aligned
   with the title, not floating between the two lines. */
.head-row:not(:has(p)) { align-items: center; }

/* The pill: a small outlined label. It is the plan name in the sidebar, the
   status of a source on the coverage grid, and the recoup state on a project.

   Outlined, not filled. A 10.5px bold label on an olive fill measures 3.78:1 —
   this was the smallest type in the sidebar sitting on the weakest contrast in
   the app. Outlining moves the colour to the edge, where the ratio is a pass
   rather than a failure, and puts the label back on the paper at 14.73:1.

   It also matches the reference sheets better than the filled version did: every
   one of them uses outlined pills and bracketed labels, and reserves solid fills
   for the single active item.

   The base edge is --border-strong. Olive belongs to the tone modifiers below,
   and that is not a preference — .up and .warn both set border-color, which is
   only a change at all if the base is something other than olive.

   There used to be a second copy of this rule up in the sidebar section, with a
   note here claiming the two were kept in sync deliberately. They were not. It
   asked for 9.5px, .15em and an olive edge, and all three lost to the values
   here for no better reason than that this block came later in the file with the
   same specificity. Every pill in the app was already drawing at 10px on a
   --border-strong edge; the duplicate had been dead in everything but
   margin-bottom for as long as both blocks said `text-transform`, and there was
   nothing on the screen that could have shown it.

   So the note was worse than nothing — it told the next person the block above
   was load-bearing, and an edit made up there in good faith would have changed
   the file and not the screen. The two are now one rule, resolved in favour of
   what was actually on the screen rather than what the dead block asked for.
   margin-bottom is the single declaration that came down from it: it was the one
   thing the block above was still contributing, because it was the one thing
   this block never repeated. */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  padding: 4px 10px; border-radius: 20px;
  background: transparent; color: var(--ink); border: 1px solid var(--border-strong);
  white-space: nowrap; font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}
.pill::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--olive); flex: none;
}
/* Up and down have to be readable across a room, and with one chromatic colour
   they can't do that by hue. So they do it by weight: "earned back" is an
   outlined chip with an olive dot, "not recouped" is a solid block of ink.

   That is a bigger visual difference than the old green-vs-blue pair, and it is
   readable to someone colourblind, in greyscale and in print. It also fixes a
   real failure — the olive version put ink on the accent at 3.78:1, under the
   4.5 a small bold label needs. Solid ink with paper on it is 14.73. */
.pill.up { color: var(--ink); border-color: var(--olive-edge); }
.pill.up::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--olive); flex: none;
}
.pill.down { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.pill.warn { color: var(--ink); border-color: var(--olive-edge); }
.pill.warn::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  border: 1.5px solid var(--olive); flex: none;
}
.pill.info { background: var(--surface-2); color: var(--ink); border-color: var(--border); }

/* ---------- Statement coverage ---------- */
/* A calendar of what has actually been paid: rows are sources, columns are
   months. Sized in a --cell variable so the whole grid retunes from one number. */
.cov-legend { display: flex; gap: 14px; font-size: 12px; color: var(--muted); white-space: nowrap; }
.cov-legend span { display: inline-flex; align-items: center; gap: 5px; }
.cov-key { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
.cov-key.on { background: var(--money); }
.cov-key.gap { background: var(--danger); }
.cov-key.off { background: var(--surface-2); border: 1px solid var(--border); }
.cov-key.locked { background: var(--surface-2); border: 1px solid var(--money); }

#coverage-grid { --cell: 13px; --gap: 2px; }
.cov-scroll { overflow-x: auto; overflow-y: hidden; padding-bottom: 4px; }
.cov-table { display: flex; flex-direction: column; gap: var(--gap); min-width: min-content; }
.cov-row { display: flex; align-items: center; }

/* Source and verdict pin to the edges while the months scroll between them, so
   you never lose track of which row you're reading. */
.cov-name {
  position: sticky; left: 0; z-index: 2; background: var(--surface);
  width: 140px; flex: none; padding-right: 12px;
  display: flex; flex-direction: column; justify-content: center; gap: 1px;
  box-shadow: 7px 0 9px -7px rgba(26, 24, 21, .22);
}
.cov-status {
  position: sticky; right: 0; z-index: 2; background: var(--surface);
  width: 144px; flex: none; padding-left: 12px;
  display: flex; align-items: center; justify-content: flex-end;
  box-shadow: -7px 0 9px -7px rgba(26, 24, 21, .22);
}
/* The pinned columns need real height in the year-rule row too, or the months
   scroll out from under them in plain sight. */
.cov-head .cov-name, .cov-head .cov-status { align-self: stretch; }
.cov-source { font-size: 13px; font-weight: 600; }
.cov-cadence { font-size: 11px; color: var(--faint); }
.cov-cells, .cov-years { display: flex; gap: var(--gap); flex: none; }
.cov-cell {
  width: var(--cell); height: var(--cell); flex: none; border-radius: 3px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.cov-cell.on { background: var(--money); border-color: transparent; }
.cov-cell.gap { background: var(--danger); border-color: transparent; }
/* Locked: the same square as "on", drained of colour. It has to be visibly a
   month you have — not a hole, which is what .gap means and what these months
   looked like before — while being visibly not money in your totals. Outlined
   rather than filled, so a row of them reads as present-but-not-counted at a
   glance without competing with the green. */
.cov-cell.locked { background: var(--surface-2); border-color: var(--money); }
/* "Not due" and "not in yet" are both fine, so both stay quiet — the row's pill
   carries the judgment rather than a wall of colour. */
.cov-cell.off { border-style: dashed; }
.cov-cell.pending { background: transparent; border-style: dashed; }
.cov-cell.pre { background: transparent; border-color: transparent; }

/* Year rules. Each spans its own months, so a short first year stays short. */
.cov-year {
  width: calc(var(--span) * var(--cell) + (var(--span) - 1) * var(--gap));
  flex: none; font-size: 11px; font-weight: 600; color: var(--faint);
  border-bottom: 1px solid var(--border); padding-bottom: 3px; margin-bottom: 3px;
  overflow: hidden; white-space: nowrap;
}
.cov-head { align-items: flex-end; }

@media (max-width: 720px) {
  .cov-legend { display: none; }
  /* 114, not 106. "SoundExchange" sets at 100px and 106 minus the padding left it
     98 — a two-pixel spill out of a sticky column, over the months scrolling
     underneath. The eight pixels come out of a strip that already scrolls
     sideways, so nothing else has to give them up. */
  .cov-name { width: 114px; padding-right: 8px; }
  .cov-status { width: 120px; padding-left: 8px; }
}

/* ---------- Goal ---------- */
.goal-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; }
.goal-num { font-size: 27px; font-weight: 700; letter-spacing: -0.7px; font-variant-numeric: tabular-nums; }
.goal-of { font-size: 13px; color: var(--muted); }
.goal-track {
  position: relative; height: 12px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 8px;
}
.goal-fill { height: 100%; background: linear-gradient(90deg, var(--money-deep), var(--money)); border-radius: 8px; transition: width .5s ease; }
/* The pace marker shows where you'd be if the year were on a flat run-rate — so
   a fill to the right of the marker means you're ahead of schedule. */
.goal-pace { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--ink); opacity: .55; }
.goal-foot { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 12.5px; color: var(--muted); }
.goal-foot b { color: var(--ink); font-variant-numeric: tabular-nums; }
.goal-edit { background: none; border: none; font: inherit; font-size: 12.5px; color: var(--accent-ink); cursor: pointer; padding: 0; text-decoration: underline; }
/* The beetle sits in the margin beside the target bar, the way a printed report
   puts a small figure out at the edge of a chart. The data stays the hero and the
   drawing is a note alongside it, costing the bar about ninety pixels.

   Below 640px it goes away entirely. The bar has a job — showing a proportion —
   and a proportion needs length to be legible. Given a phone screen and a choice
   between the mascot and the thing the mascot is commenting on, the mascot loses. */
.goal-row { display: flex; align-items: center; gap: 16px; }
.goal-body { flex: 1; min-width: 0; }
.goal-row .bug { flex: none; }
@media (max-width: 640px) { .goal-row .bug { display: none; } }

.split-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
.split-2 > .card { margin-bottom: 18px; }

/* ---------- Charts (hand-authored SVG, no chart library) ---------- */
/* Every chart in the app is drawn 900 units wide, and its type is sized so that at
   a desktop width it lands at roughly 1:1 — an 11-unit label arriving as 11px.
   Type inside an SVG is measured in viewBox units, so it scales with the picture:
   squeezed into a 300px phone column the whole drawing shrinks by two thirds and
   the month labels arrive at 3.7px, which is not small type, it is a grey smudge.

   The floor is 720px, below which the strip scrolls sideways instead of shrinking
   — the same move the coverage grid and every wide table in the app already make,
   and it holds the labels at 8.8px at their smallest. Nothing in any chart is
   drawn outside its own box, so clipping the vertical axis costs nothing. */
.chart-holder { width: 100%; overflow-x: auto; overflow-y: hidden; }
.chart-holder svg { display: block; width: 100%; min-width: 720px; height: auto; overflow: visible; }
.c-grid { stroke: var(--border); stroke-width: 1; }
.c-axis { stroke: var(--border-strong); stroke-width: 1; }
.c-tick { fill: var(--faint); font-size: 10.5px; font-family: var(--font); }
.c-tick.mid { fill: var(--muted); }
.c-label { fill: var(--muted); font-size: 11px; font-weight: 600; font-family: var(--font); }
.c-value { fill: var(--ink); font-size: 11px; font-weight: 700; font-family: var(--font); }
.c-value.pos { fill: var(--ink); }
.c-value.neg { fill: var(--danger); }
/* Bars are olive, because every bar chart in the app plots money coming in —
   income by year, earned by month, the dashboard's months — with exactly one
   exception. #spent-months is drawn by the same function from the same field, so
   it has to be named here or the spending chart would be painted in the income
   colour. Anything added later inherits olive; a new spend chart has to say so.

   In and out are told apart by value rather than hue, because there is only one
   hue: earned is solid olive, spent is ink at 72%. That reads instantly, and it
   keeps reading in greyscale, in print, and for anyone red-green colourblind —
   which the conventional green/red pairing does not. */
.c-bar { fill: var(--money); transition: fill .12s; }
.c-bar.partial { fill: rgba(107, 122, 63, .40); }
.c-bar.cost { fill: var(--spend); opacity: .72; }
.c-bar.dim { fill: var(--paper-deep); }
.c-col:hover .c-bar { fill: var(--money-deep); }
/* --spend rather than --danger, even though the two are the same ink today.
   Money leaving isn't an error, and if --danger is ever pointed at a real warning
   treatment the spending chart shouldn't quietly follow it there. */
#spent-months .c-bar { fill: var(--spend); opacity: .72; }
#spent-months .c-bar.partial { fill: var(--spend); opacity: .34; }
#spent-months .c-col:hover .c-bar { fill: var(--spend); opacity: 1; }
.c-col { cursor: default; }

/* A month on either chart opens that month. The invisible hit area does the
   work: a 3.5px dot and a thin bar are both too small to ask anyone to aim at,
   so a generous transparent shape sits over the top carrying the click. It has
   to be `fill: transparent` rather than `fill: none` — none isn't hit-testable. */
.c-hit { fill: transparent; cursor: pointer; }
.c-col.pickable { cursor: pointer; }
.c-col.pickable:hover .c-bar { fill: var(--money-deep); }
#spent-months .c-col.pickable:hover .c-bar { fill: var(--spend); opacity: 1; }
/* The hit disc sits after its dot in the markup so it lands on top, which means
   growing the dot on hover has to reach backwards — hence :has(+ …). */
.c-dot:has(+ .c-hit:hover), .c-dot:has(+ .c-hit:focus-visible) { r: 5.5; stroke-width: 2.5; }
.c-col.pickable:focus-visible, .c-hit:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }
.c-area { fill: var(--olive-soft); }
.c-area.cost { fill: var(--spend-soft); }
.c-area.money { fill: var(--money-soft); }
/* Two lines on one chart, one hue: the income line is solid olive, the cost line
   is a dashed ink one. The dash is what separates them, not the colour — so the
   chart is still readable printed in black and white, which is how anyone
   actually files these numbers at tax time. */
.c-line { fill: none; stroke: var(--olive); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }
.c-line.cost { stroke: var(--ink); stroke-opacity: .55; stroke-dasharray: 5 4; }
.c-line.money { stroke: var(--money); }
.c-dot { fill: var(--surface); stroke: var(--olive); stroke-width: 2; }
.c-dot.money { stroke: var(--money); }
.c-marker { stroke: var(--money); stroke-width: 1.5; stroke-dasharray: 3 3; }
.c-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 12px; color: var(--muted); margin-top: 10px; }
.c-legend span { display: inline-flex; align-items: center; gap: 7px; }
/* The bare key is the "Complete year" swatch on the income-by-year chart, so it
   tracks the bar colour. If it stops matching .c-bar the legend starts lying. */
.c-key { width: 12px; height: 3px; border-radius: 2px; background: var(--money); }
.c-key.cost { background: var(--spend); opacity: .72; }
.c-key.money { background: var(--money); }
.c-key.sq { width: 11px; height: 11px; border-radius: 3px; }
.c-key.partial { background: rgba(107, 122, 63, .40); }
.c-note { font-size: 12px; color: var(--muted); margin: 10px 0 0; line-height: 1.5; }
/* A note inside the chart holder is followed by more card content, so it needs
   the breathing room a trailing note in a card gets from the card padding. */
.chart-holder .c-note { margin-bottom: 18px; }
.c-note b { color: var(--ink); }

/* ---------- Compact ranked lists (best returns / underwater) ---------- */
.mini-list { display: flex; flex-direction: column; }
.mini-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 11px 8px; border-bottom: 1px solid var(--border); border-radius: 8px;
  background: none; border-left: none; border-right: none; border-top: none;
  font: inherit; text-align: left; width: 100%; cursor: pointer; transition: background .12s;
}
.mini-row:last-child { border-bottom: none; }
.mini-row:hover { background: var(--surface-2); }
.mini-main { min-width: 0; }
.mini-name { display: block; font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-meta { display: block; font-size: 11.5px; color: var(--faint); margin-top: 2px; }
.mini-side { text-align: right; flex: none; }
.mini-num { display: block; font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
.mini-num.good { color: var(--ink); }
.mini-num.bad { color: var(--danger); }

/* ---------- Positions / project tables ---------- */
/* Sits inside a .card, so it drops the outer chrome the standalone tables carry. */
.table-wrap.flush { border: none; border-radius: 0; box-shadow: none; background: none; }
.table-wrap.flush .data-table th { background: none; padding-top: 0; }
.data-table tr.clickable { cursor: pointer; }
.data-table .kind { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--faint); font-weight: 600; }
.data-table .name-cell { font-weight: 600; }
.data-table .name-cell small { display: block; font-weight: 400; font-size: 11.5px; color: var(--faint); margin-top: 2px; }
.data-table .num.neg { color: var(--danger); }
.data-table .num.pos { color: var(--ink); }
.bar-cell { width: 108px; }
.mini-track { height: 6px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.mini-fill { height: 100%; background: var(--accent); border-radius: 4px; }
.mini-fill.money { background: var(--money); }
.mini-fill.danger { background: var(--danger); }

/* ---------- Cost table ---------- */
/* Costs used to be stacked rows: payee on top, date and everything else in a
   small grey line underneath. They're a table now, same as the catalog, so each
   fact sits in its own column and reads straight down. */

/* Cells hold their line. With every column switched on the table gets wider
   than the card and the wrapper scrolls, which beats eleven columns of wrapped
   text turning every row into a two-line paragraph. */
.cost-table th, .cost-table td { white-space: nowrap; }
.cost-table .tick-cell { width: 1%; padding-left: 10px; padding-right: 10px; }
.cost-table .tick-cell .th-label { justify-content: center; }
.cost-table td.tick-cell { text-align: center; line-height: 0; }
/* Unpaid amounts stay ink. They were olive, which measured 3.25:1 on the tinted
   row underneath — the one number on this page you most need to read, set in the
   least readable colour in the app. The row is already tinted and marked, so the
   amount doesn't need to carry the state as well; it just needs to be legible. */
.cost-table .num.unpaid { color: var(--ink); }
.cost-table td.muted { color: var(--faint); }
/* Notes are free text and the only column that can run long, so it gives way
   first rather than pushing the amount off the right-hand edge. */
.cost-table .note-cell { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cost-table .flag-cell { text-align: center; color: var(--accent); font-weight: 700; }
/* Type is a column you chose to keep, not a footnote, so it takes the readable
   grey rather than the faint one — which it needs anyway on the tinted rows. */
.cost-table .kind { color: var(--muted); }
/* Unpaid rows stay tinted: it's the one thing about a cost you should be able to
   see without reading it. The tint is already as deep as the small grey type can
   carry — at this strength the date and the type label sit at 4.6:1, and one more
   step of blue drops both under 4.5. So hovering an unpaid row marks its edge
   rather than darkening its fill; the fill is spoken for. */
.cost-table tbody tr.unpaid,
.cost-table tbody tr.unpaid:hover { background: var(--warn-soft); }
.cost-table tbody tr.unpaid:hover td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
/* A tick-box that writes straight through to the DB — no save button, no form.
   One of the four things scoped for neumorphism, and the clearest case for it:
   empty it is a slot pressed into the sheet, ticked it stands proud.

   Ticked used to be an olive box with an ink ✓ on it, at 3.78:1. Ink box, paper
   ✓ instead — 14.73:1 — and the olive moves to the ring around it, where being
   a shape it measures 3.9 against a 3.0 bar and passes. */
.cost-chk {
  width: 22px; height: 22px; border-radius: 6px; border: 1.5px solid var(--border-strong);
  background: var(--surface-2); cursor: pointer; display: grid; place-items: center;
  font-size: 12px; line-height: 1; color: transparent;
  box-shadow: var(--nu-in); transition: all .16s; flex: none;
}
.cost-chk:hover { border-color: var(--olive); box-shadow: var(--nu-in-deep); }
.cost-chk.on {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
  box-shadow: var(--nu-out-sm), 0 0 0 2px var(--olive-soft);
}
.cost-chk:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }

/* ---------- Song ownership ---------- */
.own-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
.inline-field { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.inline-field label { margin: 0; }
.inline-field input[type="number"] { width: 80px; }
.pct { color: var(--muted); font-size: 13px; margin-left: -4px; }

/* ---------- Year-end report ---------- */
.rep-grid { display: flex; flex-direction: column; margin-bottom: 6px; }
.rep-line { display: flex; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
.rep-line span { color: var(--muted); }
.rep-line b { font-variant-numeric: tabular-nums; }
.rep-line.indent span { padding-left: 18px; font-size: 13px; }
.rep-line.total { border-bottom: none; border-top: 2px solid var(--border-strong); font-size: 16px; margin-top: 4px; padding-top: 14px; }
.rep-line.total span { color: var(--ink); font-weight: 600; }
.rep-line b.pos { color: var(--ink); }
.rep-line b.neg { color: var(--danger); }
/* .note-181 styled a callout about a tax deduction. Removed with the markup. */

/* ---------- Cost dialog ---------- */
.dlg {
  border: 1px solid var(--border); border-radius: var(--radius); padding: 26px;
  width: min(540px, calc(100vw - 32px)); background: var(--surface); color: var(--ink);
  box-shadow: 0 24px 60px rgba(26,24,21,.3);
}
.dlg::backdrop { background: rgba(26,24,21,.45); backdrop-filter: blur(2px); }
.dlg h2 { margin: 0 0 4px; font-size: 19px; letter-spacing: -0.3px; }
.dlg-row { display: flex; gap: 14px; }
.dlg-row .field { flex: 1; }
.chk-row {
  display: flex; align-items: flex-start; gap: 11px; padding: 11px 12px; margin-bottom: 10px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer; font-size: 13.5px; font-weight: 600;
}
.chk-row input { width: auto; margin-top: 2px; accent-color: var(--accent); flex: none; }
.chk-row small { display: block; font-weight: 400; font-size: 11.5px; color: var(--muted); margin-top: 4px; line-height: 1.5; }
.dlg-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.dlg-foot .btn.danger { margin-right: auto; }
.btn.danger { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-edge); }
.btn.danger:hover, .btn.danger:active { background: var(--danger); color: var(--bg); border-color: var(--danger); }

/* ---------- Collapsible cards ---------- */
/* The catalog is long and most of the time you don't need it, so it starts
   closed and gets out of the way of this year's numbers. */
.head-actions { display: flex; align-items: center; gap: 8px; flex: none; }

.collapse-toggle {
  width: 30px; height: 30px; flex: none; display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--muted); cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.collapse-toggle:hover { background: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent); }
.collapse-toggle .chev {
  font-size: 19px; line-height: 1; display: block;
  transform: rotate(90deg); transition: transform .18s ease;
}
.collapse-toggle[aria-expanded="true"] .chev { transform: rotate(-90deg); }
.collapsible .collapse-body { margin-top: 16px; animation: fade .18s ease; }
.collapsible .collapse-body[hidden] { display: none; }

/* ---------- Stat picker ---------- */
.stat-group { margin-bottom: 18px; }
.stat-group h3 {
  margin: 0 0 9px; font-size: 11px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--faint);
}
.stat-group .chk-row { margin-bottom: 7px; padding: 9px 11px; }
.stat-pill {
  display: inline-block; margin-left: 7px; padding: 1px 7px; border-radius: 999px;
  font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  background: var(--accent-soft); color: var(--accent-ink); vertical-align: 1px;
}

/* ---------- Projection note ---------- */
.projection {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 18px;
  margin-top: 16px; padding: 13px 15px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: 13px; color: var(--muted);
}
.projection b { color: var(--ink); font-size: 14.5px; }
.projection .sep { color: var(--faint); }

/* ---------- Settings ---------- */
/* The page behind the gear. Three cards and no controls that change anything —
   it reads rather than edits, which is why it needs almost no new furniture. */

/* A definition list, because that is what it is: three labels and their values.
   Written as <div> rows inside the <dl> so each pair can be a grid cell without
   giving up the semantics — a screen reader still hears "Email, you@example.com"
   as one thing, which a pile of spans would not have produced. */
.set-rows { margin: 0 0 20px; }
.set-row {
  display: grid; grid-template-columns: 110px 1fr; gap: 14px;
  padding: 11px 0; border-bottom: 1px solid var(--border);
}
.set-row:first-child { border-top: 1px solid var(--border); }
.set-row dt {
  font-size: 10px; font-weight: 700; color: var(--faint);
  text-transform: uppercase; letter-spacing: .14em;
  align-self: center;
}
/* dd carries a browser default margin-left of 40px that would push every value
   out of its grid column. */
.set-row dd { margin: 0; font-size: 14px; color: var(--ink); word-break: break-word; }

.set-actions { display: flex; flex-wrap: wrap; gap: 10px; }

/* The import records. Monospace and unstyled on purpose — see the note above
   loadImportEvents() in app.js. This is the one place in the app where making
   something friendlier would make it worth less, because the whole point is that
   you are seeing the row and not this app's summary of the row. */
.set-events { margin-top: 20px; }
.set-events-count {
  margin: 0 0 12px; font-size: 12px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .12em; font-weight: 700;
}
.set-events-none { margin: 0; font-size: 13.5px; color: var(--muted); }

.set-event {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  margin-bottom: 10px; overflow: hidden;
}
.set-event-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 9px 13px; background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.set-event-when { font-size: 12.5px; font-weight: 600; color: var(--ink); }
/* Outlined like the plan pill rather than filled, for the same contrast reason:
   these are 9.5px labels, and 9.5px on an olive fill measures 3.78:1. */
.set-event-tag {
  font-size: 9.5px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--border-strong);
  padding: 2px 8px; border-radius: 20px;
}
/* pre, so nothing is reflowed, and scrolling sideways rather than wrapping —
   a wrapped JSON line stops looking like the stored value and starts looking
   like prose about it. */
.set-event-body {
  margin: 0; padding: 13px; overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.55; color: var(--ink);
  white-space: pre; tab-size: 2;
}

@media (max-width: 560px) {
  /* Label above value. At 110px the label column costs more than half a phone
     screen and every email address wraps three times. */
  .set-row { grid-template-columns: 1fr; gap: 4px; }
}

/* ---------- Responsive ---------- */
@media (max-width: 1040px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
  .split-2 { grid-template-columns: 1fr; }
  /* The second selector is not redundant. `.head-row:not(:has(p))` above scores
     a class plus an element, so a bare `.head-row` here loses to it and the
     stacked title and buttons end up centred. Matching its specificity and
     coming later in the file is what lets stretch win. Head-rows without a
     sub-line used to be rare; they are the normal case now. */
  .head-row, .head-row:not(:has(p)) { flex-direction: column; align-items: stretch; }
  .own-grid { grid-template-columns: 1fr; }
  .dlg-row { flex-direction: column; gap: 0; }
  .subtabs { overflow-x: auto; }
  /* The cost table scrolls sideways on a narrow screen rather than reflowing,
     the same as the catalog table it now matches. */
  .cost-table { min-width: 560px; }
  .cost-table .note-cell { max-width: 160px; }
}
@media (max-width: 560px) {
  .stat-grid { grid-template-columns: 1fr; }
}

@media (max-width: 860px) {
  .app { flex-direction: column; }
  .sidebar { width: 100%; height: auto; position: static; flex-direction: row; align-items: center; flex-wrap: wrap; padding: 14px 16px; }
  .brand { padding: 0 12px 0 0; }
  .nav { flex-direction: row; flex-wrap: wrap; margin: 0; }
  /* This used to be display:none here, which was fine while it held a standing
     "Local only" badge — losing a decorative label on a narrow screen costs
     nothing. It now holds the account you are signed in as and the gear that is
     the only way to reach settings, the privacy policy and the way out, so
     hiding it stranded anyone on a phone. It folds into the horizontal bar
     instead, keeping the name and the gear and dropping only the email, which is
     the one part too long for a single row. */
  .sidebar-foot {
    display: flex; align-items: center; gap: 12px;
    margin: 0 0 0 auto; padding: 0; border-top: 0; width: auto;
  }
  .sidebar-foot .pill { margin-bottom: 0; }
  .acct-mail { display: none; }
  .main { padding: 24px 18px 60px; }
  .panel-grid { grid-template-columns: 1fr; }
  .field-row { flex-direction: column; gap: 15px; }
  .map-row { grid-template-columns: 1fr; gap: 8px; }
  .song-row { grid-template-columns: 1fr auto; gap: 6px 12px; }
  .song-name { grid-column: 1 / -1; white-space: normal; }
  .song-sources { grid-column: 1; }
  .song-amt { grid-column: 2; }
  .artist-albums .album-assign { grid-column: 1 / -1; width: 100%; }
  .pro-row { grid-template-columns: 1fr; gap: 8px; }
  .pro-actions { justify-content: flex-start; }
  .pro-statusbar { flex-direction: column; align-items: flex-start; }
  .edu-two { grid-template-columns: 1fr; gap: 14px; }
}

/* ===========================================================================
   Signed out: the landing page, sign in, sign up
   ===========================================================================
   These screens share the app's tokens rather than defining their own, which is
   the reason the palette lives in :root. When the palette changes, the front
   door changes with it and nobody has to remember to go and update it. */

.out {
  min-height: 100vh; display: flex; flex-direction: column;
  background: var(--bg); color: var(--ink);
}

/* --- masthead ------------------------------------------------------------- */

.out-top {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 22px 34px; border-bottom: 1px solid var(--border);
}
.out-brand {
  display: inline-flex; align-items: center; gap: 11px;
  text-decoration: none; color: inherit;
}
/* The mark is /icon.svg — the same file the browser tab loads, not a copy of it.
   That is the point. This was the letters IW in an ink tile, which meant the tab,
   this header and the beetle on the page below were three unrelated things all
   claiming to be one product. Pointing at the favicon leaves exactly one drawing;
   test/icon.test.js already holds that file to beetle.js, so the header cannot
   drift from the tab because there is nothing here to drift.

   No background, no radius, no colour: the artwork brings its own olive disc, and
   an img ignores `color` anyway. Sizing only. */
.out-mark {
  width: 30px; height: 30px; flex: none;
  display: block;
}
.out-word { font-family: var(--serif); font-size: 17px; letter-spacing: -.01em; }
.out-top nav { display: flex; align-items: center; gap: 10px; }

/* Ghost buttons get a resting edge out here, and only out here. In the app the
   rule is "no edge until you reach for it", which works because a ghost button
   there sits in a row of other buttons — the context does the explaining. On
   these pages it stands alone beside prose, where a transparent border made the
   Free tier's only call to action ("Create account") read as a line of text.
   --border-strong is 3.43:1 on paper, so the edge clears the 3:1 non-text
   threshold. No new colour, and the app's buttons are untouched. */
.out .btn.ghost { border-color: var(--border-strong); color: var(--ink); }
.out .btn.ghost:hover { background: var(--surface); box-shadow: var(--nu-out-sm); }

/* --- the auth card ------------------------------------------------------- */

.out-mid { flex: 1; display: grid; place-items: center; padding: 46px 22px 64px; }

/* 384px is a measure, not a round number: at 14px sans it puts the email field
   at roughly 46 characters, which is wide enough for a long address without the
   form sprawling into something that looks like a settings page. */
.auth {
  width: 100%; max-width: 384px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 30px 30px 26px;
}
.auth-head { text-align: center; margin-bottom: 22px; }
/* The bracketed label from the app's plate, reused so the two halves of the
   product feel like one product. */
.auth-eyebrow {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .18em; color: var(--faint);
}
.auth-eyebrow::before { content: "[\00a0"; }
.auth-eyebrow::after { content: "\00a0]"; }
/* 26px clears the 22px floor the serif needs to hold its thin strokes. */
.auth h1 {
  font-family: var(--serif); font-weight: 500;
  font-size: 26px; line-height: 1.18; letter-spacing: -.01em;
  margin: 9px 0 0;
}
.auth-head p {
  margin: 8px 0 0; font-size: 13px; color: var(--muted); line-height: 1.5;
}
/* The beetle is the one drawn thing on these screens, and it earns its place:
   arriving at a sign-in page is a moment, and it is where the mascot introduces
   itself before you have any data for it to push. */
.auth-bug { display: flex; justify-content: center; margin: 2px 0 -4px; }
.auth form { margin-top: 4px; }
.auth .btn { width: 100%; justify-content: center; margin-top: 4px; }

/* A row under the label for the "forgot?" link, so it sits with the field it
   belongs to instead of floating at the bottom of the form. */
.auth .field .lbl-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.auth .field .lbl-row label { margin-bottom: 0; }

.auth-alt {
  margin: 18px 0 0; padding-top: 16px; border-top: 1px solid var(--border);
  text-align: center; font-size: 13px; color: var(--muted);
}
/* The inline link, everywhere rather than only on the signed-out pages.

   This was `.out a.lnk` for as long as every inline link in the app happened to
   sit on a signed-out page, which made the scope look deliberate when it was
   only a description of where links had been needed so far. The settings page
   then grew one, got the class, matched nothing, and rendered in #0000EE — the
   browser default, and the single loudest colour in a palette that otherwise
   runs to olive and bone. It reads as a bug in a page about how to get help.

   Unscoping cannot change anything that already exists: every .lnk in the
   project is on a page carrying the .out wrapper, so all of them matched this
   rule before and match it now, with the same declarations. */
a.lnk {
  color: var(--ink); text-decoration: underline;
  text-decoration-color: var(--olive-edge); text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}
a.lnk:hover { text-decoration-color: var(--olive); }
a.lnk:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; border-radius: 2px; }
.auth-tiny {
  margin: 14px 0 0; font-size: 11.5px; color: var(--faint);
  line-height: 1.5; text-align: center;
}

/* --- messages ------------------------------------------------------------ */

/* Errors are ink on a faint ink wash with an olive rule down the edge — the
   app's established "something went wrong" shape. No red: with one chromatic
   colour there is no red to reach for, and the olive rule plus the sentence
   carry the signal without relying on hue at all. */
.auth-msg {
  display: none; margin: 0 0 16px; padding: 10px 12px;
  border: 1px solid var(--border); border-left: 3px solid var(--olive);
  border-radius: var(--radius-sm); background: var(--danger-soft);
  font-size: 12.5px; line-height: 1.5; color: var(--ink);
}
.auth-msg.on { display: block; }

/* --- the demo credentials note ------------------------------------------- */

/* Only rendered on localhost — see login.html. A convenience while the startup
   flow is being built, not a thing to ship to strangers. */
.auth-demo {
  margin: 16px 0 0; padding: 12px 13px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--nu-in);
  font-size: 12px; color: var(--muted); line-height: 1.55;
}
.auth-demo strong {
  display: block; font-size: 9.5px; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--faint); margin-bottom: 7px;
}
.auth-demo button {
  display: block; width: 100%; text-align: left; margin-top: 5px;
  font-family: var(--mono); font-size: 11.5px; color: var(--ink);
  background: none; border: 0; border-radius: 4px; padding: 4px 5px; cursor: pointer;
}
.auth-demo button:hover { background: var(--olive-soft); }
.auth-demo button:focus-visible { outline: 2px solid var(--olive); outline-offset: 1px; }
.auth-demo button span { color: var(--faint); }

/* --- landing ------------------------------------------------------------- */

/* Named "pitch", not "hero". The app already owns .hero (line ~643) — the ink
   masthead over the dashboard total, with paper-coloured text and its own
   .hero .btn.ghost overrides. Reusing that name here inherited the ink slab,
   which put grey body copy on near-black at about 2:1 and made the primary CTA
   (ink fill, paper text) invisible against it. Same lesson as the JS globals:
   one name, one home. */
.pitch { max-width: 1080px; margin: 0 auto; padding: 62px 34px 30px; }
.pitch-in {
  display: grid; grid-template-columns: 1.15fr .85fr; gap: 40px; align-items: center;
}
.pitch h1 {
  font-family: var(--serif); font-weight: 500;
  /* Fluid, with a hard floor above the 22px the didone needs and a ceiling that
     keeps the line from outgrowing its measure. */
  font-size: clamp(34px, 5.4vw, 56px); line-height: 1.06; letter-spacing: -.02em;
  margin: 14px 0 0; max-width: 22ch;
}
.pitch p.lede {
  margin: 18px 0 0; font-size: 16px; line-height: 1.6;
  color: var(--muted); max-width: 46ch;
}
.pitch-cta { display: flex; flex-wrap: wrap; gap: 11px; margin-top: 26px; }
.pitch-cta .btn { padding: 11px 20px; font-size: 14px; }
.pitch-art { display: grid; place-items: center; }
.pitch-note {
  margin: 12px 0 0; font-size: 12px; color: var(--faint);
  text-align: center; max-width: 30ch; line-height: 1.5;
}

.strip { border-top: 1px solid var(--border); background: var(--paper-lo); }
.strip-in {
  max-width: 1080px; margin: 0 auto; padding: 46px 34px;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 34px;
}
.strip h3 {
  font-family: var(--serif); font-weight: 500; font-size: 22px;
  letter-spacing: -.01em; margin: 0 0 8px;
}
.strip p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
/* A rule above each column instead of a box around it. Editorial structure is
   made of lines meeting, not of containers. */
.strip-col { border-top: 2px solid var(--ink); padding-top: 14px; }

.tiers { max-width: 1080px; margin: 0 auto; padding: 58px 34px 74px; }
.tiers-head { text-align: center; margin-bottom: 30px; }
/* The bare h2 rule (line ~434) makes section headings small, tracked, uppercase
   and grey, which is right inside the app where h2 labels a panel. Here it is a
   display line, so it has to opt out of the transform and the grey explicitly —
   overriding font-size alone left it shouting in muted caps. */
.tiers-head h2 {
  font-family: var(--serif); font-weight: 500; font-size: 30px;
  letter-spacing: -.015em; margin: 8px 0 0;
  text-transform: none; color: var(--ink);
}
.tier-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px; max-width: 720px; margin: 0 auto;
}
.tier {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 24px 24px 26px;
  display: flex; flex-direction: column;
}
/* The paid tier is marked by an ink rule at its head, matching how the app marks
   anything selected. Olive would have been the instinct and it cannot carry a
   word, so the rule is ink and the badge beside it does the talking. */
.tier.lead { border-top: 3px solid var(--ink); }
.tier-name {
  display: flex; align-items: baseline; gap: 9px;
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .16em; color: var(--faint);
}
.tier-badge {
  font-size: 9px; letter-spacing: .12em; color: var(--ink);
  background: var(--olive-swipe); border-radius: 3px; padding: 2px 6px;
}
.tier-price {
  font-family: var(--serif); font-size: 34px; font-weight: 500;
  letter-spacing: -.02em; margin: 12px 0 2px;
}
.tier-price small { font-family: var(--sans); font-size: 13px; color: var(--muted); letter-spacing: 0; }
.tier ul { list-style: none; margin: 16px 0 22px; padding: 0; }
.tier li {
  position: relative; padding: 0 0 0 20px; margin-bottom: 9px;
  font-size: 13.5px; line-height: 1.5; color: var(--ink);
}
/* An olive disc as the marker: a shape, which is what olive is for. */
.tier li::before {
  content: ""; position: absolute; left: 2px; top: 7px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--olive);
}
.tier li.off { color: var(--muted); }
.tier li.off::before { background: none; border: 1.5px solid var(--border-strong); }
.tier .btn { margin-top: auto; width: 100%; justify-content: center; }
/* The line admitting the paid plan is not switched on yet. Under the grid rather
   than inside the paid card: it is a fact about the state of the whole product,
   not a property of one tier, and inside the card it would read as fine print
   attached to the price — which is the one place a reader is entitled to assume
   nothing is being hidden. */
.tiers-note {
  max-width: 720px; margin: 18px auto 0; text-align: center;
  font-size: 12.5px; line-height: 1.6; color: var(--muted);
}

/* --- long-form prose: the privacy page ------------------------------------
   No card, no panel, no box. Everything else on the signed-out side is a
   container with a border round it, because everything else is a form or a
   choice. This is neither — it is a document somebody opened to read, and the
   fastest way to make a document look like something to be skipped is to put it
   in the same rounded rectangle as the pricing tiers.

   So the structure here is a measure and a set of rules, the way a printed page
   does it. 68ch is the one number that matters: at 15px it puts a line at
   roughly 70 characters, which is about where the eye stops finding the start of
   the next line reliably. Wider reads as a terms-of-service wall, which is the
   precise impression this page exists to avoid. */
.doc { max-width: 68ch; margin: 0 auto; padding: 54px 34px 76px; }
.doc h1 {
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(30px, 4.4vw, 40px); line-height: 1.12; letter-spacing: -.02em;
  margin: 12px 0 0;
}
.doc p.lede { margin: 18px 0 0; font-size: 16px; line-height: 1.62; color: var(--muted); }
/* The date sits under the lede rather than in the masthead, and it is the
   smallest thing on the page on purpose: it matters to the two people who want
   to know whether this changed, and to nobody else reading it for the first
   time. */
.doc-stamp {
  margin: 20px 0 0; padding-top: 16px; border-top: 1px solid var(--border);
  font-size: 11.5px; color: var(--faint);
}
/* An olive rule above each section, which is the same move the landing page's
   three columns make — structure from lines meeting rather than from boxes. It
   is olive here and ink there because these headings are quieter than a pitch
   and the rule is a shape, not type, which is the only thing olive is allowed to
   be. */
.doc h2 {
  font-family: var(--serif); font-weight: 500; font-size: 23px;
  letter-spacing: -.012em; text-transform: none; color: var(--ink);
  margin: 46px 0 0; padding-top: 15px; border-top: 2px solid var(--olive);
}
.doc h3 {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .1em; color: var(--muted);
  margin: 30px 0 0;
}
.doc p { margin: 14px 0 0; font-size: 15px; line-height: 1.68; color: var(--ink); }
.doc h2 + p, .doc h3 + p, .doc h3 + ul { margin-top: 12px; }
.doc strong { font-weight: 700; }
.doc em { font-style: italic; color: var(--muted); }
.doc ul { list-style: none; margin: 14px 0 0; padding: 0; }
.doc li {
  position: relative; padding: 0 0 0 22px; margin-top: 10px;
  font-size: 15px; line-height: 1.6; color: var(--ink);
}
/* The same olive disc the pricing tiers use, so a list of what is stored and a
   list of what a plan includes are visibly the same kind of list. */
.doc li::before {
  content: ""; position: absolute; left: 2px; top: 9px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--olive);
}
/* The one sentence on the page that has to survive skimming: the promise the
   import log is built around. Given the weight of a pull quote rather than being
   left as another paragraph, because a reader who reads exactly one line of this
   section should read this one. Ink type on the olive wash — the fill is a
   shape, the words are ink, which keeps the palette rule intact and the contrast
   at 11:1. */
/* p.doc-rule, not .doc-rule, and the extra character is load-bearing. `.doc p`
   above is (0,1,1) and a lone `.doc-rule` is (0,1,0), so the plain class lost
   every property the two of them both set — the pull quote rendered at the body
   size of 15px inside a box built for something twice that, which reads as a
   paragraph that has been highlighted rather than as the one line on the page
   that matters. Caught by looking at it; nothing about the CSS is wrong enough to
   fail anything. */
p.doc-rule {
  margin: 22px 0 0; padding: 20px 24px;
  background: var(--olive-soft); border-left: 3px solid var(--olive);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-family: var(--serif); font-weight: 500;
  font-size: 21px; line-height: 1.42; letter-spacing: -.01em;
  color: var(--ink);
}

.out-foot {
  border-top: 1px solid var(--border); padding: 26px 34px;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 14px; font-size: 12px; color: var(--faint);
}
.out-foot nav { display: flex; gap: 18px; }

@media (max-width: 820px) {
  .out-top { padding: 18px 20px; }
  .pitch { padding: 40px 20px 24px; }
  .pitch-in { grid-template-columns: 1fr; gap: 26px; }
  /* The beetle leads on a narrow screen. It is the one thing that says what
     this is before any words have been read. */
  .pitch-art { order: -1; }
  .pitch p.lede { max-width: none; }
  .strip-in { grid-template-columns: 1fr; gap: 26px; padding: 34px 20px; }
  .tiers { padding: 40px 20px 54px; }
  .tier-grid { grid-template-columns: 1fr; }
  .out-mid { padding: 30px 16px 46px; }
  .auth { padding: 24px 22px 22px; }
  .doc { padding: 36px 20px 54px; }
  .out-foot { padding: 22px 20px; }
}

/* ---------- Onboarding: the roll, the tour, the corner beetle ---------- */

/* This whole block obeys the same rule as the rest of the file: olive fills
   shapes and never a letter. The beetle's ball, the step dots, the pipes in the
   sources diagram and the little accordion crosses are olive. Every word in
   here — including the labels sitting inside the olive boxes — is ink. */

/* --- the roll ---------------------------------------------------------------

   A full-bleed sheet of paper with one hairline of ground across it. The beetle
   enters from the right and pushes its growing ball off the left edge. Nothing
   else is on screen: no spinner, no logo, no percentage. The animation IS the
   product's one idea, so anything beside it is competition. */

/* Nothing behind a takeover should scroll. Without this the app underneath keeps
   its scrollbar, and a stray wheel gesture during the tour leaves the person
   somewhere else entirely by the time it closes. */
body:has(.ob-intro), body:has(.ob-tour) { overflow: hidden; }

.ob-intro {
  position: fixed; inset: 0; z-index: 300;
  background: var(--paper);
  overflow: hidden;
  opacity: 1; transition: opacity .4s ease;
  cursor: pointer;
}
.ob-intro.out { opacity: 0; }

/* Fading out at both ends so the floor reads as continuing past the screen
   rather than stopping dead at two arbitrary vertical lines. */
.ob-ground {
  position: absolute; left: 0; right: 0; bottom: 46vh; height: 1.5px;
  background: linear-gradient(90deg,
    transparent, var(--border-strong) 10%, var(--border-strong) 90%, transparent);
}

/* Offset by 21px because the beetle's feet are not at the bottom of its own
   drawing: the ground rule sits at y=124 of a 144-tall viewBox, which at this
   width leaves 21px of empty box below the animal. Without this the beetle
   walks along 21px above its own floor. */
.ob-roller {
  position: absolute; left: 0; bottom: calc(46vh - 21px);
  transform: translate3d(104vw, 0, 0);
  will-change: transform;
}
.ob-roller .bug { display: block; }

/* --- the tour --------------------------------------------------------------- */

.ob-tour {
  position: fixed; inset: 0; z-index: 200;
  background: var(--paper);
  display: grid; place-items: center;
  padding: 40px 34px 104px;
  opacity: 0; transition: opacity .32s ease;
  overflow-y: auto;
}
.ob-tour.in { opacity: 1; }

.ob-card {
  width: 100%; max-width: 860px;
  display: grid; grid-template-columns: 340px 1fr; gap: 44px;
  align-items: center;
  transform: translateY(8px);
  transition: transform .32s ease;
}
.ob-tour.in .ob-card { transform: none; }

/* The art column is sized for the widest thing that goes in it (the sources
   diagram at 340px) and the narrower beetles centre inside it. Fixing the column
   is what stops the copy jumping sideways as you page through the five screens. */
.ob-art { display: grid; place-items: center; }
.ob-art .bug, .ob-art .ob-sources { display: block; }

.ob-copy h2 {
  font-family: var(--serif); font-size: 34px; font-weight: 500;
  line-height: 1.15; letter-spacing: -.02em; color: var(--ink);
  margin: 0 0 14px;
}
.ob-lede { font-size: 16px; line-height: 1.6; color: var(--ink); margin: 0 0 18px; }

.ob-list { list-style: none; margin: 0 0 18px; padding: 0; display: flex; flex-direction: column; gap: 13px; }
.ob-list li { position: relative; padding-left: 22px; }
/* An olive disc as the marker, matching the pricing tiers. A shape, which is
   what olive is for. */
.ob-list li::before {
  content: ""; position: absolute; left: 0; top: 6px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--olive);
}
.ob-list strong { display: block; font-size: 14.5px; font-weight: 650; color: var(--ink); }
.ob-list span { display: block; font-size: 13.5px; line-height: 1.5; color: var(--muted); margin-top: 2px; }

/* The kicker line. An olive rule down its edge rather than a box, because the
   sentence is meant to land as an aside that turns out to be the point. */
.ob-note {
  margin: 0 0 18px; padding: 2px 0 2px 14px;
  border-left: 2px solid var(--olive);
  font-size: 14px; line-height: 1.55; color: var(--ink);
}

.ob-actions { display: flex; align-items: center; gap: 20px; margin-top: 26px; flex-wrap: wrap; }
.ob-next { padding: 11px 20px; font-size: 14px; }

/* Pinned to the overlay rather than flowing under the card, so the dots hold
   still while the card above them changes height from screen to screen. */
.ob-foot {
  position: absolute; left: 0; right: 0; bottom: 34px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 34px;
}
.ob-dots { display: flex; align-items: center; gap: 10px; }
.ob-dot {
  width: 9px; height: 9px; padding: 0; border-radius: 50%;
  background: none; border: 1.5px solid var(--border-strong);
  cursor: pointer; transition: background .18s, border-color .18s, transform .18s;
}
.ob-dot:hover { border-color: var(--olive); }
.ob-dot.on { background: var(--olive); border-color: var(--olive); transform: scale(1.25); }
.ob-dot:focus-visible { outline: 2px solid var(--olive); outline-offset: 3px; }
.ob-skip { position: absolute; right: 34px; font-size: 13px; }

/* --- the sources diagram ---------------------------------------------------- */

/* A wash rather than a solid fill. Ink on solid olive measures 3.78:1 and fails
   for type; ink over olive at this alpha stays above 10:1, so the four boxes can
   hold their own labels without the labels stopping being ink. */
.ob-src-box { fill: var(--olive-swipe); stroke: var(--olive-edge); stroke-width: 1.2; }
.ob-src-text { fill: var(--ink); font-family: var(--sans); font-size: 12.5px; font-weight: 600; }
/* Dashed, so they read as routes money travels down rather than as structural
   rules of the kind the rest of the app draws in solid hairline. */
.ob-src-pipe { fill: none; stroke: var(--olive); stroke-width: 1.6; stroke-dasharray: 3 4; opacity: .65; }

/* --- the corner beetle ------------------------------------------------------ */

/* Sits below the toast stack on purpose. A toast is something the app needs you
   to read; the beetle is something you may or may not ever want. */
.ob-helper { position: fixed; right: 18px; bottom: 10px; z-index: 90; }

.ob-sleeper {
  position: relative; display: block; padding: 0; line-height: 0;
  background: none; border: 0; cursor: pointer;
  transition: transform .2s ease;
}
.ob-sleeper:hover { transform: translateY(-3px); }
.ob-sleeper:focus-visible { outline: 2px solid var(--olive); outline-offset: 4px; border-radius: var(--radius-sm); }
.ob-sleeper .bug { display: block; animation: obBreathe 4.6s ease-in-out infinite; transform-origin: 62% 88%; }

/* Two z's on staggered delays. Ink, not olive — they are letters. */
.ob-sleeper::before, .ob-sleeper::after {
  content: "z"; position: absolute;
  font-family: var(--serif); font-style: italic; color: var(--muted);
  pointer-events: none; opacity: 0;
  animation: obZ 3.4s ease-out infinite;
}
.ob-sleeper::before { right: 16px; top: 6px; font-size: 13px; }
.ob-sleeper::after  { right: 4px;  top: 0;   font-size: 17px; animation-delay: 1.1s; }

/* Awake: he stops breathing in his sleep and the z's go. */
.ob-helper.open .ob-sleeper .bug { animation: none; }
.ob-helper.open .ob-sleeper::before, .ob-helper.open .ob-sleeper::after { animation: none; opacity: 0; }

/* Played once when the tour ends, so the beetle is visibly seen arriving in the
   corner rather than just being there next time you look. */
.ob-helper.arriving .ob-sleeper .bug { animation: obArrive .9s cubic-bezier(.2,.8,.3,1); }

@keyframes obBreathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); } }
@keyframes obArrive { from { transform: translateX(70px) scale(.9); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes obZ {
  0%   { opacity: 0; transform: translate(0, 0) scale(.7); }
  25%  { opacity: .85; }
  100% { opacity: 0; transform: translate(7px, -22px) scale(1.1); }
}

/* --- the help panel --------------------------------------------------------- */

.ob-panel {
  position: absolute; right: 0; bottom: calc(100% + 12px);
  width: 344px; max-height: min(70vh, 560px); overflow-y: auto;
  background: var(--paper-hi); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 16px 18px 18px;
  animation: obPanel .18s ease-out;
}
@keyframes obPanel { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

.ob-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.ob-panel-head h2 { font-family: var(--serif); font-size: 18px; font-weight: 500; margin: 0; color: var(--ink); }
.ob-x {
  background: none; border: 0; padding: 0 2px; line-height: 1;
  font-size: 21px; color: var(--muted); cursor: pointer; transition: color .15s;
}
.ob-x:hover { color: var(--ink); }
.ob-x:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }

.ob-here {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: var(--olive-soft); border: 1px solid var(--olive-edge);
  border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 14px;
  transition: background .15s;
}
.ob-here:hover { background: var(--olive-swipe); }
.ob-here:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }
.ob-here strong { display: block; font-size: 13.5px; font-weight: 650; color: var(--ink); }
.ob-here span { display: block; font-size: 12.5px; color: var(--muted); margin-top: 2px; }

.ob-faq { display: flex; flex-direction: column; }
.ob-faq-item { border-top: 1px solid var(--border); }
.ob-faq-item:last-child { border-bottom: 1px solid var(--border); }

.ob-q {
  position: relative; display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; padding: 11px 26px 11px 0;
  font-family: inherit; font-size: 13.5px; font-weight: 600; line-height: 1.4; color: var(--ink);
}
.ob-q:hover { color: var(--olive-deep); }
.ob-q:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }
/* A drawn plus, not a typed one: two olive bars, the upright collapsing to a
   minus when the answer is open. Olive is allowed here because these are
   rectangles, not characters. */
.ob-q::before, .ob-q::after {
  content: ""; position: absolute; right: 4px; top: 50%;
  background: var(--olive); border-radius: 1px;
  transition: transform .18s ease;
}
.ob-q::before { width: 11px; height: 2px; margin-top: -1px; }
.ob-q::after  { width: 2px; height: 11px; margin-top: -5.5px; margin-right: 4.5px; }
.ob-q[aria-expanded="true"]::after { transform: scaleY(0); }

.ob-answer {
  max-height: 0; overflow: hidden; opacity: 0; margin: 0;
  font-size: 13px; line-height: 1.6; color: var(--muted);
  transition: max-height .24s ease, opacity .2s ease, padding .24s ease;
}
.ob-answer.open { max-height: 340px; opacity: 1; padding-bottom: 12px; }

.ob-panel-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border);
}
.ob-mute {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: 0; padding: 4px 2px; cursor: pointer;
  font-family: inherit; font-size: 12px; color: var(--muted); transition: color .15s;
}
.ob-mute:hover { color: var(--ink); }
.ob-mute:focus-visible { outline: 2px solid var(--olive); outline-offset: 2px; }
/* Filled dot when sound is on, hollow when it is off. The state is in the word
   beside it too — the dot is confirmation, never the only signal. */
.ob-mute::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: var(--olive); border: 1.5px solid var(--olive);
}
.ob-mute[aria-pressed="true"]::before { background: none; border-color: var(--border-strong); }

/* The toast stack normally sits in this corner. Lift it above the beetle only
   when the beetle is actually on the page. */
body:has(.ob-helper) .toast-wrap { bottom: 96px; }

@media (max-width: 860px) {
  .ob-card { grid-template-columns: 1fr; gap: 22px; max-width: 460px; }
  /* The beetle leads on a narrow screen, same as the marketing page: it says
     what this is before a word has been read. */
  .ob-art { order: -1; }
  .ob-art .ob-sources { width: 100%; height: auto; }
  .ob-copy h2 { font-size: 27px; }
  .ob-tour { padding: 30px 20px 96px; }
  .ob-panel { width: min(344px, calc(100vw - 36px)); }
}

/* Somebody who has asked for less movement gets the beetle, but not the journey,
   the breathing or the z's. */
@media (prefers-reduced-motion: reduce) {
  .ob-sleeper .bug, .ob-helper.arriving .ob-sleeper .bug { animation: none; }
  .ob-sleeper::before, .ob-sleeper::after { animation: none; opacity: 0; }
  .ob-panel { animation: none; }
  .ob-card { transform: none; }

  /* The working ring stops turning but does NOT disappear. Everything else in
     this block is decoration somebody has asked to do without; this one is
     load-bearing — it is the only thing on screen saying an import is still
     running, and removing it would put this person back in front of the button
     that looked broken. A complete olive ring, held still, next to a label that
     reads "Importing…". */
  .btn.is-working::before { animation: none; border-top-color: var(--olive); }
}

/* ---------- Dashboard: custom range ---------- */

.range-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  margin-bottom: 18px;
}
.range-bar label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .14em;
  color: var(--faint); margin: 0;
}
/* Two selectors to out-specify the global input width:100%. */
.range-bar input[type="month"] { width: auto; min-width: 152px; padding: 8px 10px; font-size: 13.5px; }
.range-note { margin-left: auto; font-size: 12.5px; color: var(--muted); }

/* A ranked list where the bar is the row's own background rather than a separate
   column, so the name sits on top of the quantity it describes and no horizontal
   space is spent twice. */
.range-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.range-list li {
  position: relative; display: grid; grid-template-columns: 1fr auto auto;
  align-items: baseline; gap: 12px; padding: 9px 11px;
  font-size: 13.5px; border-radius: var(--radius-sm);
}
.rl-bar {
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 0;
  background: var(--olive-soft); border-radius: var(--radius-sm);
}
.rl-name, .rl-amt, .rl-pct { position: relative; z-index: 1; }
.rl-name { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rl-amt { font-weight: 650; color: var(--ink); font-variant-numeric: tabular-nums; }
.rl-pct { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 42px; text-align: right; }
/* The remainder of a list that was cut at eight. It carries no bar, because it
   isn't one payer and drawing it as one would invite a comparison that means
   nothing — it's here so the column adds up, not to be ranked. */
.range-list .rl-rest { margin-top: 3px; border-top: 1px solid var(--border); border-radius: 0; }
.range-list .rl-rest .rl-name, .range-list .rl-rest .rl-amt { color: var(--muted); font-weight: 500; }

/* --- what a total covers --------------------------------------------------
   The one line that qualifies every number on every screen when a free account
   is at its upload limit. See the comment on #coverage-note in app.html for why
   it is one element and not a label on each total.

   Deliberately not styled as an error. It is not one: nothing has gone wrong,
   no number on the page is incorrect, and the artist has not done anything they
   need to undo. A red panel here would say "something is broken", they would
   look for the break, and the actual message — that the total covers two files
   — is the part they would skim past.

   Olive, which is the one colour in this app that carries meaning rather than
   decoration, and it is always paired with words. The left rule does the work
   of drawing the eye; the text does the work of explaining. */
.coverage-note {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: -6px 0 20px;
  padding: 10px 14px;
  background: var(--warn-soft);
  border: 1px solid var(--warn-edge);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-sm);
}
.coverage-note p {
  margin: 0; flex: 1 1 320px;
  font-size: 13px; line-height: 1.45; color: var(--ink);
}
.coverage-note .btn { flex: 0 0 auto; }

/* ---------- PRO claiming: the locked cross-check ---------- */
/* The panel a free account gets in place of the three piles. Deliberately a
   plain card and not a warning, an error or a paywall: nothing has gone wrong,
   nothing of theirs is missing, and no number on the screen is incorrect. The
   only thing that is true is that a feature they haven't paid for isn't running.

   No olive rule, no red edge, no lock icon. This app spends its warning colour
   on money that has gone missing; spending it here would make it mean "either a
   statement is missing or you're on the free plan", and a signal that means two
   things means neither.

   Left-aligned, and that is the whole of the layout thinking. The first version
   centred everything to match the beetle notes elsewhere, which is right for one
   line under a drawing and wrong for four paragraphs: centred body copy gives
   the eye no fixed left edge to return to, so each line has to be found before
   it can be read. This panel exists to be read. */
.pro-locked { max-width: 62ch; }
/* Serif, sentence case, borrowed from .explain-text h2 rather than invented.
   The bare h2 rule is 12px uppercase with wide tracking — a label, sized for
   "STATEMENT COVERAGE" sitting over a grid. A whole sentence set that way is a
   headline shouted through a wall, and this heading has to be a sentence:
   the two-word version of it is already in the tag directly above.

   Two classes deep, and it has to be. The precedent this copies, .explain-text
   h2, lives outside a card and so only ever argues with the bare h2 rule. This
   heading is inside one, and `.card h2` (line 1644) sets 15.5px with -0.2px
   tracking at a specificity of (0,1,1) — which a single class cannot beat. The
   first attempt was `.pro-locked-head` alone; text-transform took, because the
   bare h2 is only (0,0,1), and the size and tracking silently did not. It read
   as a sentence at label size, which looks like a font choice rather than a bug,
   and is exactly the kind of thing that ships. Not `h2.pro-locked-head`: that
   ties `.card h2` at (0,1,1) and wins only on being further down the file, so
   the next rule anybody adds above it takes the heading back. */
.pro-locked .pro-locked-head {
  font-family: var(--serif); font-size: 20px; font-weight: 500;
  text-transform: none; letter-spacing: 0; color: var(--ink);
  margin: 10px 0 14px; line-height: 1.3;
}
.pro-locked-line {
  margin: 0 0 12px;
  font-size: 13.5px; line-height: 1.6; color: var(--ink);
}
.pro-locked-line:last-child { margin-bottom: 0; }
.pro-locked-line.muted { color: var(--muted); font-size: 13px; }
.pro-locked-line a { color: inherit; text-underline-offset: 2px; }
