:root{
  /* backstage at low light: neutral warm charcoal */
  --bg:#141519; --bg-2:#181a1f; --surface:#1d2026; --surface-2:#242830;
  /* --surface-1 is the lift a row takes when the pointer is over it, or when it
     is the selected one. It was used by five rules and never defined, so every
     one of them resolved to nothing: hovering a door row did visibly nothing,
     and a picked place on the career map lost its fill and kept only its rail.
     It sits between --surface and --surface-2 on purpose. It is feedback, not
     depth: a row that lifts must not read as a card that was raised. */
  --surface-1:#22262e;
  /* Lines you can actually see. --line was 1.23:1 against --surface, which is
     under the point where an edge reads at all on a dark screen, so every card
     border in the app was doing nothing and the surfaces had to separate by a
     1.1:1 fill on their own. */
  --line:#3a404d; --line-soft:#2c313a;
  /* THE DULLEST INK CARRIED HALF THE APP.
     Measured on the dashboard, 27 Jul 2026: 52% of all text on the page was
     --ink-3, against 27% --ink and 18% --ink-2. At #727a87 that colour sits at
     3.77:1 on --surface, below the 4.5 AA floor, so more than half the words
     in this product were rendered in the one ink too dim to read comfortably.
     That is the whole of "everything looks dull and hard to read".
     #87909e is 5.06:1 on --surface and 5.66 on --bg, and still clearly the
     quietest of the three: 13.1 / 7.6 / 5.1 is a legible ladder. */
  --ink:#e9e6df; --ink-2:#aab1bd; --ink-3:#87909e;
  /* THE ROOMS RUN A SPECTRUM, AND IT STARTS AT THE FRONT DOOR.

     The same six colours this product has always used, reassigned on 3 Aug 2026
     so the order you meet them in the nav IS the order of the spectrum, read
     backwards from purple: Overview, Contacts, Career, Catalog, Revenue,
     Messages. Nothing new was invented and nothing was dropped; every hue here
     was already in the palette, wearing a different room.

     Before, the assignment was historical rather than ordered, so the sidebar
     went copper, teal, blue, violet, sand, rose: six colours in no sequence, and
     no help at all in telling where you were. Now moving down the nav moves
     along the spectrum, and a room's colour says how far into the app it sits.

     The signature moves with it. Overview is the front door, so --brand is the
     purple the front door wears, and the knob on the fader mark is purple too.
     Anything that falls back to --brand, a primary button or a focus ring on a
     surface with no room of its own, now falls back to purple. */
  --brand:#9a92e0;
  /* domain accents, muted so nothing shouts */
  --acc-home:var(--brand); /* dashboard + report : the signature, purple */
  --acc-book:#6f9bd1;     /* contacts, orgs, inbox, connections : blue */
  --acc-career:#5fb3a6;   /* gigs, releases, wishlist, strategy : green */
  --acc-catalog:#cdb079;  /* tracks + setlists : yellow */
  --acc-revenue:#d08a63;  /* money : orange */
  --acc-voice:#d18f93;    /* drafts : red */
  /* THE CLOSENESS RAMP: ONE FAMILY, THREE STRENGTHS, AND THE FAMILY IS RED.

     Fourth attempt. All three earlier ones were mine and each fixed the last
     one's fault while introducing its own.

     1. Amber, sage and slate. Two of those existed nowhere else in the app.
        Kiana: "i absolutely hate these colors. they don't belong to our brand
        palette."
     2. So I used palette members, and picked #d08a63, #cdb079, #6f9bd1, which
        are BYTE-IDENTICAL to --acc-revenue, --acc-catalog and --acc-book.
        Three rooms' accents now meant something that is not a room: on a
        Contacts card, "warm" wore Revenue. Kiana: "the colors of the cooling
        off are still not in place."
     3. So I made it one ordered ramp, and reached for Contacts blue because
        closeness is a Contacts fact. That fixed the collision and broke the
        WORD. Kiana: "you use a cold color blue for representing something
        warm. rather use the red brand color palette."

     4. So I used the brand red, --acc-voice, the dusty rose Messages wears.
        Kiana: "these are not the red colors i talked about." Shown the five
        warm tokens the palette actually holds, she chose #de8f45, --warn's.
     5. Which was the right FAMILY and the wrong member. "the warmest orange
        needs to be the exact same color as our main color orange."

     The main orange is --acc-revenue. It is the orange in the six-room
     spectrum, the one the nav draws, the one Revenue's cards and its "Log an
     entry" door wear. --warn is a semantic token that happens to be orange,
     which is not the same thing as the brand's orange, and I offered it in a
     list without saying so.

     The scale is named warm, cooling, cold: a reader decodes the temperature
     before they read the legend, so the hue has to agree with the word or the
     legend is arguing with the picture.

     It stays a single ordered ramp on one family, because closeness is one
     ordered dimension. It fades toward the LIGHT, not toward the surface:
     --warn at full for warm, then mixed toward --ink, the app's off-white.

     That direction was the sixth correction and it matters. Mixing an orange
     toward a near-black surface does not lighten it, it MUDDIES it: cooling
     and cold came out brown, three swatches of much the same dull colour, and
     Kiana sent a picture of them. "orange to fading slowly to white but still
     in the orange region." Mixed toward --ink instead, the same orange runs
     strong, then peach, then a pale cream, which is what a thing cooling off
     should look like: the colour drains out of it rather than the light.

     Saturation carries the order here, not lightness, which is the honest
     encoding for a scale whose bottom end means "barely anything". "No touches
     yet" stays a neutral grey outside the ramp, because an absence is not a
     temperature, and grey against three oranges cannot be mistaken for the
     ramp's next step.

     Anchoring on --acc-revenue also undoes something version 5 had to accept:
     it briefly re-merged warm with warn, which were split apart once before
     precisely because the healthiest closeness band and the needs-attention
     state were wearing one pixel with opposite meanings. They are different
     colours again, without anyone having to remember why. */
  --warm:var(--acc-revenue);
  --cooling:color-mix(in srgb, var(--acc-revenue) 60%, var(--ink));
  --cold:color-mix(in srgb, var(--acc-revenue) 28%, var(--ink));
  /* Absence is NEUTRAL, deliberately outside the ramp rather than the bottom
     of it. First attempt made it the faintest blue step, which measured 1.09
     against cold: the same dark smudge twice, and both barely clearing the
     card at 1.87 and 1.72. It is grey now, so it separates by hue from three
     blues and does not have to win a lightness argument it cannot win. This is
     what the original note meant by "an absence is not a temperature"; the
     mistake was reading that as a licence to give it the loudest neutral in
     the palette. The band rails do not use this at all, because there absence
     is a short tick rather than a full-height bar and shape says it first. */
  --band-none:color-mix(in srgb, var(--ink-3) 62%, var(--surface));
  /* --warn was byte-identical to --warm, so the healthiest closeness band and
     the needs-attention state wore one pixel with opposite meanings, sometimes
     on one screen. Warn is its own orange now: 6.31:1 on --surface, 11.6 dE
     from warm, unmistakably a sibling and unmistakably not the same word.
     --danger stays where it is on purpose: every deeper red fails the 4.5:1
     floor for the text it colours (form errors, canceled badges). */
  --ok:#6cc6a0; --warn:#de8f45; --danger:#d07368;
  --radius:14px; --radius-s:9px;
  --shadow:0 1px 0 rgba(255,255,255,.02), 0 10px 30px -18px rgba(0,0,0,.7);
  --t:.18s ease;
}
/* layout extras, kept apart so the token block above stays byte-true to the spec */
:root{ --topbar-h:57px; }

/* ---------- the spacing scale ----------

   Kiana, 26 Jul 2026: "there's a bit crowdiness, messyness, elements too close
   to each other."

   The cause was measurable, and it was one thing. Every container in the app
   used the same value for the space INSIDE it and the space BETWEEN it and the
   next one: cards 18px padding in a grid with an 18px gap, panels the same,
   split columns the same. When those two distances are equal the eye has
   nothing to group by, so eleven separate boxes read as one crowded field. The
   app also carried 29 distinct spacing values, including 6/7/8/9px and
   10/11/12/13/14px, which no reader can tell apart but which stop anything
   from lining up.

   So: one scale, and one rule on top of it.

   THE RULE: the gap between two boxes is always visibly larger than the
   padding inside them. Nothing on a page may space its siblings the same
   distance it spaces its own contents.

   The scale is multiples of four. Micro-nudges of 1-3px that do optical work
   (a baseline shift, a border compensation) are left alone; they are not
   spacing, they are alignment. */
:root{
  --s1:4px; --s2:8px; --s3:12px; --s4:16px;
  --s5:20px; --s6:24px; --s7:28px; --s8:40px;
  --pad-card:var(--s5);      /* inside a card or a panel */
  --gap-box:var(--s7);       /* between two cards or panels: 28 against 20 */
  --gap-col:var(--s8);       /* between layout columns */
  --gap-row:var(--s3);       /* inside one row, between its own parts */
}
/* Chart-grade colors, validated for the dark surface with the six-check dataviz
   validator (lightness band 0.48-0.67, chroma floor 0.1, CVD separation,
   normal-vision floor, contrast vs --surface).

   These are NOT the room accents. The accents are deliberately muted and fail
   two of the checks outright: #6f9bd1 sits at chroma 0.094 and #cdb079 at
   lightness 0.77, so as data marks they read washed out and
   too close together. What the chart tokens must do is sit in the SAME HUE
   FAMILY as the room they appear in, deepened until they pass.

   Revised 27 Jul 2026 after Kiana read the career chart as off-brand, and she
   was right about both marks:
     --ch-blue #4f83cd -> #6690cd   the rooms' blue, deepened
                                    (was a generic dashboard blue)
     --ch-gold #b28a2f -> #bf8340   a warm amber in the sand family
                                    (was an olive mustard in no family at all)
   Both re-validated as a pair: worst adjacent CVD dE 19.9 protan / 20.8 tritan,
   normal-vision 21.4, every check PASS. --ch-teal cannot be lightened toward the
   rooms' green without leaving the lightness band, so it stays. */
/* --ch-violet joined 28 Jul 2026 as the catalog room's own chart family, so
   its completeness bars would stop borrowing another room's hue. Validated as
   a five-token set with the dataviz six-check script: all five in the
   lightness band, chroma >= 0.1, worst adjacent CVD dE 11.1 deutan / 15.9
   tritan, normal vision 21.4, all >= 3:1 on --surface. */

/* WHICH ROOM EACH CHART TOKEN BELONGS TO, AFTER 3 AUG 2026.

   The measurements in the notes above are facts about the colours and stand
   unchanged. The FAMILIES they name do not: the rooms were reassigned that day
   into spectrum order, so every "--ch-x is in the --acc-y family" claim above
   describes the old arrangement. Read them as history. The families now:

     --ch-violet  Overview   (--brand, purple)
     --ch-blue    Contacts   (--acc-book)
     --ch-teal    Career     (--acc-career, green)
     --ch-gold    Catalog    (--acc-catalog, yellow)
     --ch-copper  Revenue    (--acc-revenue, orange)

   Drafts has no chart of its own and so needs no token. The five are still a
   CATEGORICAL palette first: a breakdown inside one room picks whichever four
   tell its cards apart, and only a mark that carries a ROOM'S FACT, a night, a
   touch, a payment, is obliged to wear that room's hue. */
/* --ch-gold re-derived 2 Aug 2026: at #bf8340 it sat 14.2 dE from copper, the
   set's closest pair, on ADJACENT categories of one mapping, while claiming
   membership of the sand family it was 23 dE from. #c79a45 doubles the copper
   gap to 25.3, moves within 19.7 of sand, and reads 6.32:1 on the surface. */
:root{ --ch-teal:#2fa893; --ch-copper:#cd7440; --ch-blue:#6690cd; --ch-gold:#c79a45; --ch-violet:#8b80e8; }

/* HIDDEN MEANS HIDDEN.

   The UA stylesheet's [hidden]{display:none} is the weakest rule in the
   cascade, so any class that sets a display beats it and an element the code
   believes is hidden renders anyway. This has bitten three times already, each
   patched one selector at a time: .scrim, .rep-monthfield and .sl-picker all
   carry a hand-written [hidden] rule, and setlist.css even writes down why.

   The fourth was the Log a touch modal, where .form-grid{display:grid} kept
   the "somebody new" name and organization fields on screen at all times. The
   modal offered a picker AND a name field AND an organization field at once,
   so the one flow that is supposed to be the shortest path to a logged touch
   opened asking three questions where it meant to ask one. 514 by 76 pixels of
   form that every line of JS around it treated as hidden.

   One rule, once, so the next component that sets a display cannot reintroduce
   it. !important because that is the only thing a class selector cannot beat,
   and there is no case in this app where something carries the hidden
   attribute and is meant to be seen. */
[hidden]{display:none !important}

/* A card that DOES something says what it has done, and keeps saying it. A
   toast lasts three seconds; this line stays, so pressing a button on Settings
   leaves visible evidence on the card you pressed it from. */
.settings-state{
  margin:var(--s3) 0 0;
  font-size:12px; letter-spacing:.02em;
  color:var(--page, var(--brand));
}
.settings-state:empty{display:none}

/* ---------- base ---------- */

/* THE NATIVE CONTROLS WERE STILL IN LIGHT MODE.

   Nothing ever declared a colour scheme, so the browser drew every control it
   owns for a white page. On the setlist form that meant the two UNSELECTED
   radios rendered as bright white discs while the selected one was a modest
   violet ring: the two wrong answers were the loudest things in the group. The
   same applied to checkboxes, the date picker panel, and the dropdown lists
   inside every select in the app.

   One declaration fixes all of them at once, and `accent-color` then puts the
   room's own colour on the checked state instead of the system blue. */
:root{ color-scheme:dark }
input, select, textarea, button{ accent-color:var(--page,var(--brand)) }

*{box-sizing:border-box; scrollbar-width:thin; scrollbar-color:var(--line) transparent}
::-webkit-scrollbar{width:9px; height:9px}
::-webkit-scrollbar-thumb{background:var(--line); border-radius:99px}
::-webkit-scrollbar-track{background:transparent}
html{scroll-behavior:smooth}
body{
  margin:0;
  background:
    radial-gradient(1100px 540px at 82% -12%, color-mix(in srgb, var(--brand) 5%, transparent), transparent 60%),
    radial-gradient(850px 480px at -8% 34%, color-mix(in srgb, var(--acc-book) 4%, transparent), transparent 55%),
    var(--bg);
  color:var(--ink);
  font-family:"Inter",system-ui,-apple-system,sans-serif;
  /* --fz kept as the one place the body size is written, and nothing steps it
     any more. The topbar had a button that cycled it through three sizes;
     Kiana, 4 Sep 2026: "the absolute most unnecessary feature ever." A reader
     who wants larger text has a browser that does it better, across every site
     they own, without a control of ours in the way. */
  font-size:var(--fz, 15.5px);
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  letter-spacing:.004em;
  min-height:100vh;
}
h1,h2,h3,h4{font-family:"Space Grotesk",system-ui,sans-serif; font-weight:600; line-height:1.18; letter-spacing:-.01em; margin:0}
h1{font-size:clamp(26px,3.4vw,34px)}
h2{font-size:20px}
h3{font-size:17px}
p{margin:0 0 12px}
em{font-family:"Fraunces",Georgia,serif; font-style:italic; font-weight:500; color:var(--page,var(--brand))}
.mono{font-family:"JetBrains Mono",ui-monospace,monospace}
.muted{color:var(--ink-3)}
/* THE one caption style. Chart and figure captions across the app say
   class="quiet", and until 25 Aug 2026 no global rule existed: pages that
   carried their own scoped .quiet looked right and every other caption
   rendered as full-size body ink, loudest text on the block. One rule, and
   the scoped ones that agree with it can retire over time. */
.quiet{color:var(--ink-3); font-size:13.5px; line-height:1.55}
a{color:inherit; transition:color var(--t)}
a:hover{color:var(--page,var(--brand))}

/* THE NAME IS THE DOOR, AND IT HAS TO LOOK LIKE ONE.

   Across the dashboard, contacts, career, the catalog, rooms and the report,
   72 links were rendered in plain --ink with no underline, no border and no
   background: pixel-identical to the body text beside them. Every one of them
   was a person, a room, a track or a night, which is to say the primary thing
   a reader wants to click on every page in this app. The only signal they
   carried was a colour change on hover, and hover does not exist on a phone,
   which is where this thing gets installed.

   One rule, centrally, so it cannot drift again. Anything that already carries
   its own affordance opts out by class: buttons, chips, stat tiles, the room
   corner tags, badges, tabs, whole-card links, graph nodes and rows that are
   doors in their entirety. The underline is quiet on purpose - a third of the
   room accent - and comes up to full strength under the pointer. */
/* THE QUIET LINK OUT OF A PANEL.

   A small uppercase mono link that names the room it opens. It had exactly one
   definition, `.dash .plink` in dashboard.css, and the rule above deliberately
   excludes .plink from the underline every other in-app link gets. So the two
   .plink links on Career's Places tab, which sit nowhere near .dash, rendered
   as plain body text: no colour, no underline, no affordance of any kind. It
   is a shared component, so it is defined where the shared components live.
   Only the panel-head's margin-left:auto stays scoped to the dashboard. */
.plink{
  font-size:10.5px; letter-spacing:.1em; text-transform:uppercase;
  color:color-mix(in srgb, var(--page,var(--brand)) 55%, var(--ink-3));
  text-decoration:none; white-space:nowrap;
}
.plink:hover{color:var(--page,var(--brand))}
.plink.more{display:inline-block; margin-top:12px}

main a[href^="#/"]:not(.btn):not(.chip):not(.stat):not(.plink):not(.badge):not(.tab):not(.seg):not(.card):not(.rowdoor):not(.web-node):not(.nav-link):not(.wl-org):not(.gig-ra){
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-decoration-color:color-mix(in srgb, var(--page,var(--brand)) 34%, transparent);
  text-underline-offset:3px;
}
main a[href^="#/"]:hover{text-decoration-color:var(--page,var(--brand))}
::selection{background:color-mix(in srgb, var(--brand) 30%, transparent)}
:focus-visible{outline:2px solid var(--page,var(--brand)); outline-offset:2px; border-radius:4px}
.row{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.grow{flex:1}
.hidden{display:none !important}

/* ---------- topbar ---------- */
.topbar{
  position:sticky; top:0; z-index:60;
  display:flex; align-items:center; gap:13px;
  min-height:var(--topbar-h);
  padding:0 18px;
  background:color-mix(in srgb, var(--bg) 84%, transparent);
  backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
  border-bottom:1px solid var(--line-soft);
}
.topbar .spacer{flex:1}
.brand{display:flex; align-items:center; gap:10px; text-decoration:none; color:var(--ink)}
.brand:hover{color:var(--ink)}
.brand .word{font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:17px; letter-spacing:.01em; line-height:1}
.brand .ctx{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px; letter-spacing:.08em; color:var(--ink-3); margin-top:2px}
.mark{display:block; flex:none}
.mark .mk-track{fill:color-mix(in srgb, var(--ink-3) 35%, var(--line))}
.mark .mk-knob{fill:var(--ink-3)}
.mark .mk-knob.lit{fill:var(--brand)}
.menu-btn{
  display:none; align-items:center; justify-content:center;
  width:36px; height:36px; flex:none;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-s);
  color:var(--ink-2); cursor:pointer; transition:border-color var(--t), color var(--t);
}
.menu-btn:hover{border-color:var(--brand); color:var(--ink)}
.tool-btn{
  display:inline-flex; align-items:center; justify-content:center;
  width:36px; height:36px; flex:none;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-s);
  color:var(--ink-2); cursor:pointer;
  font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:13.5px; line-height:1;
  transition:border-color var(--t), color var(--t);
}
.tool-btn:hover{border-color:var(--brand); color:var(--ink)}
.tool-btn sup{font-size:10.5px; color:var(--ink-3); margin-left:1px}
.tool-btn[aria-expanded="true"]{border-color:var(--brand); color:var(--ink)}

/* THE ADD MENU. Nine doors that used to be a panel on the dashboard (TRIM.md,
   17 Aug 2026), now a menu under the + in the topbar on every page. It hangs
   from the topbar's bottom edge at the right, one column, each door in the
   colour of the room it opens (App.wearRoom sets --page per item, so the dot
   is the only place a room's accent shows: the label stays in ink so nine
   rows do not read as nine coloured buttons). On a phone it spans the width. */
.add-menu{
  position:fixed; top:calc(var(--topbar-h) + 6px); right:14px; z-index:70;
  min-width:220px; padding:6px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow);
  display:flex; flex-direction:column; gap:2px;
}
.add-menu-item{
  display:flex; align-items:center; gap:10px;
  padding:9px 12px; border-radius:var(--radius-s);
  color:var(--ink); text-decoration:none; font-size:14px; line-height:1.2;
}
.add-menu-item::before{
  content:""; width:8px; height:8px; border-radius:50%; flex:none;
  background:var(--page, var(--brand));
}
.add-menu-item:hover, .add-menu-item:focus-visible{background:var(--surface-2); outline:none}
@media(max-width:560px){
  .add-menu{left:12px; right:12px; min-width:0}
}

/* The unverified address, said where the address is. Revenue's orange, the
   same hue the trial strip uses when something is owed: this is a thing to
   deal with, not an error. */
.idnote.unverified{
  display:flex; align-items:center; flex-wrap:wrap; gap:var(--s2);
  padding:9px 12px; border-radius:var(--radius-s);
  background:color-mix(in srgb, var(--acc-revenue) 10%, var(--surface));
  border:1px solid color-mix(in srgb, var(--acc-revenue) 26%, var(--line-soft));
  color:var(--ink-2);
}

/* TWO-STEP SIGN-IN. The typed key and the recovery codes are the two things a
   person copies by hand, so both are mono, spaced, and selectable. The codes
   are the only place in this app that says "write this down", because they are
   the only thing that cannot be shown twice. */
.tfa-secret{
  font-size:14px; letter-spacing:.12em; word-break:break-all;
  padding:10px 12px; border-radius:var(--radius-s);
  background:var(--surface-2); border:1px solid var(--line-soft); color:var(--ink);
  user-select:all;
}
.tfa-codes{
  margin-top:var(--s5); padding:var(--s4);
  border-radius:var(--radius-s);
  background:color-mix(in srgb, var(--acc-revenue) 9%, var(--surface));
  border:1px solid color-mix(in srgb, var(--acc-revenue) 26%, var(--line-soft));
}
.tfa-codes ul{
  list-style:none; margin:var(--s3) 0 0; padding:0;
  display:grid; grid-template-columns:repeat(auto-fit, minmax(128px, 1fr)); gap:var(--s2);
}
.tfa-codes li{
  font-size:14px; letter-spacing:.08em; user-select:all;
  padding:6px 8px; border-radius:6px; background:var(--surface-2); color:var(--ink);
}

/* THE TRIAL STRIP. One quiet line under the topbar, the width of the app. It
   wears the signature purple while the week is running and Revenue's orange
   once it is up, because at that point it is about money owed and orange is
   the room money lives in. Never red: a trial ending is not an error. */
.trialbar{
  display:flex; align-items:center; justify-content:space-between; gap:var(--s4);
  padding:9px clamp(20px,4vw,56px);
  background:color-mix(in srgb, var(--brand) 12%, var(--bg-2));
  border-bottom:1px solid color-mix(in srgb, var(--brand) 28%, var(--line-soft));
  font-size:13px; line-height:1.35;
}
.trialbar.over{
  background:color-mix(in srgb, var(--acc-revenue) 12%, var(--bg-2));
  border-bottom-color:color-mix(in srgb, var(--acc-revenue) 30%, var(--line-soft));
}
.trialbar-text{color:var(--ink)}
.trialbar-link{
  color:var(--ink-2); text-decoration:none; font-weight:500; white-space:nowrap;
}
.trialbar-link:hover{color:var(--ink)}
@media(max-width:560px){
  .trialbar{padding-left:16px; padding-right:16px; font-size:12.5px}
}

/* THE BELL. A tool button like the others, with a dot that lights when a
   teammate has changed something since you last looked. The dot wears the
   signature purple, not a warning red: a colleague adding a contact is news,
   not an alarm. */
.bell-btn{position:relative}
.bell-dot{
  position:absolute; top:5px; right:5px;
  width:8px; height:8px; border-radius:50%;
  background:var(--brand);
  box-shadow:0 0 0 2px var(--bg-2);
}
.bell-menu{
  min-width:288px; max-width:min(360px, calc(100vw - 28px));
  max-height:min(60vh, 420px); overflow-y:auto;
  padding:6px;
}
.bell-head{
  font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:12px;
  text-transform:uppercase; letter-spacing:.07em; color:var(--ink-3);
  padding:8px 12px 6px;
}
.bell-item{
  display:flex; flex-direction:column; gap:1px;
  padding:8px 12px; border-radius:var(--radius-s);
}
.bell-item + .bell-item{border-top:1px solid var(--line-soft)}
.bell-what{font-size:13.5px; line-height:1.35; color:var(--ink)}
.bell-when{font-size:11.5px}
/* an unseen line carries the same purple tick the add-menu dots use, on the
   left, so what is new this open reads at a glance */
.bell-item.unseen{position:relative}
.bell-item.unseen .bell-what{font-weight:500}
.bell-item.unseen::before{
  content:""; position:absolute; left:4px; top:12px;
  width:5px; height:5px; border-radius:50%; background:var(--brand);
}
.bell-empty{padding:10px 12px; font-size:13px}
@media(max-width:560px){
  .bell-menu{left:12px; right:12px; min-width:0; max-width:none}
}
.id-chip{
  display:inline-flex; align-items:center; gap:9px;
  background:var(--surface); border:1px solid var(--line); border-radius:99px;
  padding:4px 13px 4px 5px; color:var(--ink-2); cursor:pointer;
  font-family:inherit; font-size:13.5px;
  transition:border-color var(--t), color var(--t);
}
.id-chip:hover{border-color:var(--brand); color:var(--ink)}
.id-chip .ava{
  width:23px; height:23px; border-radius:99px; flex:none;
  display:grid; place-items:center;
  background:color-mix(in srgb, var(--brand) 20%, var(--surface-2));
  color:var(--brand);
  font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:12px; text-transform:uppercase;
}
.id-chip .role{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3)}
@media(max-width:560px){ .id-chip .role{display:none} .brand .ctx{display:none} }
/* The narrowest phone still in use is 320px, and there the topbar row (menu,
   brand, two 44px tool buttons and the identity chip) came to four pixels more
   than the viewport, which is enough to make the whole PAGE scroll sideways on
   every view in the app. The gutter gives those pixels back, and the name
   truncates rather than pushing, so a long one cannot bring it back. */
@media(max-width:380px){
  .topbar{padding:0 11px; gap:9px}
  .id-chip{min-width:0}
  .id-chip .id-name{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
}

/* ---------- shell + sidebar ---------- */
.shell{display:grid; grid-template-columns:232px minmax(0,1fr); align-items:start}
/* THE NAV SCROLLS, THE SCROLLBAR DOES NOT SHOW.

   The sidebar is a fixed-height sticky column, so the moment the nav is even a
   few pixels taller than the viewport the browser draws a track down its whole
   edge. With the overflow that small the thumb fills almost the entire track,
   which does not read as a scrollbar at all: it reads as a thick grey rule
   between the nav and the page, sitting right beside the app's own 1px
   border. Kiana sent a screenshot of it: "this needs to be removed. that
   scroll bar is not needed."

   overflow-y stays `auto`, because a short window or a long nav must still be
   able to reach the bottom item; only the BAR is hidden. That is the one place
   in this app where hiding a scrollbar is right, because the sidebar's content
   is a fixed, short list a reader can see the end of, so the bar was never
   carrying information about how much more there is. The page's own scrollbar
   is untouched, where that information is real. */
.sidebar{
  position:sticky; top:var(--topbar-h); align-self:start;
  height:calc(100vh - var(--topbar-h)); overflow-y:auto;
  scrollbar-width:none;          /* Firefox, and the modern standard */
  -ms-overflow-style:none;       /* old Edge */
  display:flex; flex-direction:column;
  padding:20px 12px 22px 18px;
  border-right:1px solid var(--line-soft);
}
/* WebKit needs its own pseudo-element; `width:0` rather than `display:none` so
   the element still exists and the scroll position stays addressable. */
.sidebar::-webkit-scrollbar{width:0; height:0}
.sidebar .grp{
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.12em; text-transform:uppercase;
  color:var(--ink-3); margin:16px 0 6px 9px;
}
.sidebar .nav-main .grp:first-child{margin-top:2px}
.sidebar a{
  display:flex; align-items:center; gap:10px;
  padding:7px 10px; border-radius:8px; text-decoration:none;
  color:var(--ink-2); font-size:13.5px;
  border-left:2px solid transparent;
  transition:background var(--t), color var(--t);
}
.sidebar a:hover{background:var(--surface); color:var(--ink)}
.sidebar a.active{
  background:var(--surface-2); color:var(--ink);
  border-left-color:var(--dot,var(--brand));
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--dot,var(--brand)) 16%, transparent);
}
.sidebar a .dot{width:8px; height:8px; border-radius:99px; background:var(--dot,var(--brand)); flex:none; opacity:.85}

/* The room's own mark, in the room's own accent, at the same 8px optical
   weight the dot had so nothing in the rail moves. It sits quieter than the
   label until the row is hovered or current, because twelve marks all at full
   strength is a toolbar, not a rail. */
.sidebar a .nav-mark{
  display:inline-flex; align-items:center; justify-content:center;
  width:16px; height:16px; flex:none;
  color:var(--dot,var(--brand)); opacity:.72;
  transition:opacity var(--t);
}
.sidebar a:hover .nav-mark, .sidebar a.active .nav-mark{opacity:1}
.nav-sys{margin-top:auto; padding-top:12px; border-top:1px solid var(--line-soft)}
.scrim{position:fixed; inset:0; top:var(--topbar-h); z-index:70; background:color-mix(in srgb, var(--bg) 55%, transparent); backdrop-filter:blur(2px)}
.scrim[hidden]{display:none}

/* ---------- main + view header ---------- */
main{padding:0 clamp(20px,4vw,56px) 120px; max-width:1180px; min-width:0}
.view{animation:fade .35s cubic-bezier(.2,.7,.2,1) both}
@keyframes fade{from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:none}}
.kicker{
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--page,var(--brand)); margin:0 0 10px;
}
.viewhead{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:18px; flex-wrap:wrap;
  padding:42px 0 26px; margin-bottom:26px;
  border-bottom:1px solid var(--line-soft);
}
.viewhead .lede{color:var(--ink-2); margin:8px 0 0; max-width:62ch; font-size:15px}
/* SIGNAL'S FACT LINE, ONE RULE FOR EVERY ROOM (27 Aug 2026).

   Career and Revenue each carried their own copy of this, in their own room
   stylesheet, under their own name. Adding it to the Dashboard, both Directory
   pages and Tracks would have made five copies of one declaration, and five
   copies is how "the same on every page" stops being true without anybody
   editing anything. It is one class now and every room uses it. */
.viewhead .viewfacts{color:var(--ink-3); font-size:12.5px; letter-spacing:.04em}

/* ---------- the import preview (27 Aug 2026) ----------

   A phone book is hundreds of rows, so the preview scrolls inside itself
   rather than growing the dialog past the screen: .modal-body already caps
   and scrolls, and a second scroller here keeps the counts and the buttons
   in view while the reader checks the list. Rows that will not be added are
   dimmed rather than hidden, because "already in your book" is the answer to
   a question the reader is about to ask. */
.imp-lede{color:var(--ink-2); margin:0 0 var(--s4)}
.imp-preview{max-height:46vh; overflow:auto; border:1px solid var(--line); border-radius:var(--radius-s)}
.imp-preview .table{margin:0}
.imp-row-off{opacity:.55}
/* margin-left:auto keeps the actions on the right even when the header wraps
   them onto their own line. Revenue wraps first, because it carries three
   buttons and an extra sentence, and it was the page that looked like it had
   chosen a different corner. It had not; it just ran out of room sooner. */
.viewhead .actions{display:flex; gap:10px; flex-wrap:wrap; padding-bottom:3px; margin-left:auto}

/* entity detail pages */
.detail-head{
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:18px; flex-wrap:wrap;
  padding:30px 0 24px; margin-bottom:26px;
  border-bottom:1px solid var(--line-soft);
}
.detail-head .back{
  display:inline-flex; align-items:center; gap:7px;
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-3); text-decoration:none; margin-bottom:14px;
}
/* the arrow is App.icon("arrow-left", { size: 14 }), prepended by the view as
   a real child; the 7px gap above is the standard icon gap */
.detail-head .back:hover{color:var(--ink)}
.detail-head .meta{display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:12px}
.detail-head .actions{display:flex; gap:10px; flex-wrap:wrap}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  background:var(--surface); border:1px solid var(--line); color:var(--ink);
  border-radius:var(--radius-s); padding:8px 15px;
  font-family:inherit; font-size:13.5px; font-weight:500; line-height:1.35;
  cursor:pointer; text-decoration:none; white-space:nowrap;
  transition:border-color var(--t), background var(--t), color var(--t);
}
.btn:hover{border-color:var(--page,var(--brand)); color:var(--ink)}
.btn.primary{
  background:var(--page,var(--brand)); border-color:transparent;
  color:var(--bg); font-weight:600;
}
.btn.primary:hover{background:color-mix(in srgb, var(--page,var(--brand)) 86%, var(--ink)); color:var(--bg)}
/* a resting edge, so a ghost reads as pressable before the pointer proves it */
.btn.ghost{background:transparent; border-color:var(--line-soft); color:var(--ink-2)}
.btn.ghost:hover{background:var(--surface); color:var(--ink)}
.btn.danger{
  background:transparent;
  border-color:color-mix(in srgb, var(--danger) 50%, var(--line));
  color:var(--danger);
}
.btn.danger:hover{background:color-mix(in srgb, var(--danger) 13%, transparent); border-color:var(--danger); color:var(--danger)}

/* THE ROW-LEVEL DESTRUCTIVE ACTION, ONE WEIGHT FOR THE WHOLE APP.

   A Delete sitting in a list row next to an Edit is not the page's headline
   act, and drawing it in --danger made it the loudest thing on 442 gig rows and
   on every royalty row. Career decided this and went quiet; Revenue kept a
   private .rev-del that painted the same button red, so the identical control
   shouted on one page and receded on another. It reads as ordinary text until
   the pointer is on it, and only then admits what it does.

   This lives here rather than in career.css because it is now used by career,
   revenue, strategy, drafts and setlist. .btn.danger stays for the deliberate,
   page-level destructive button (wipe the demo data, remove the examples),
   which SHOULD be loud. */
.btn.danger-quiet{color:var(--ink-3)}
.btn.danger-quiet:hover{color:var(--danger); border-color:color-mix(in srgb, var(--danger) 45%, var(--line))}
.btn.small{padding:4px 11px; font-size:12px; border-radius:7px}
.btn:disabled{opacity:.45; cursor:default; pointer-events:none}
.btn svg{flex:none}

/* ---------- icons ---------- */
/* App.icon draws on a 16-unit grid and sets no colour of its own: the root
   carries stroke:currentColor, so an icon takes the colour of the text it
   sits in. Inside .muted it goes --ink-3, inside .badge.danger it goes
   --danger, inside .kicker it goes --page, with no per-icon CSS and no way
   for a hex to escape :root.
   Four sizes, no others: 14 inline with mono metadata and small controls,
   16 the default, 20 the view-head mark, 28 the empty-state mark. Stroke
   width is in viewBox units per size, so rendered weight stays near constant
   (about 1.5px at 14 and 16, 1.6px at 20, 1.75px at 28), a deliberate slight
   grade so the large marks do not look wispy. Not vector-effect:
   non-scaling-stroke, which holds the weight dead flat and leaves the 28px
   mark looking hollow.
   The standard gap beside text is 7px, the gap .btn already sets. */
.ic{display:inline-block; vertical-align:-.15em; flex:none}
.ic-14{width:14px; height:14px; stroke-width:1.7}
.ic-16{width:16px; height:16px; stroke-width:1.5}
.ic-20{width:20px; height:20px; stroke-width:1.3}
.ic-28{width:28px; height:28px; stroke-width:1.0}
/* an icon trailing inline text, where there is no flex row to carry the gap.
   Same 7px, not a third value.

   The link that carries it holds the line together: on a narrow card the meta
   line broke between "ra.co" and its mark and left the mark alone on the next
   row, where it read as a stray glyph rather than as "this link leaves the
   app". A link and the mark that describes it are one word. */
.ic-trail{margin-left:7px}
a:has(> .ic-trail){white-space:nowrap}

/* The view-head mark is one 20px icon left of the .kicker text, in every room,
   so the kicker has to be a flex line or the mark towers over the 11px text
   with no gap. This is the twin of the .detail-head .back rule below, which
   ships the same display/align/gap trio. flex, not inline-flex: the kicker
   keeps its block-level box, so a head with no mark lays out exactly as it did
   before. Scoped to .viewhead because .kicker also names many card heads. */
.viewhead .kicker{display:flex; align-items:center; gap:7px}

/* ---------- dashed means the app worked it out ---------- */
/* One law, no icon, no new token, no colour: dashed is derived, solid is
   what you told it. It applies only at text scale, on a value, a badge or a
   callout rule. That scale is what holds it apart from .empty, whose dashed
   border is chrome around a whole block at --line, and it is the joint to
   keep narrow: do not widen this to whole blocks.
   Where a reader first meets it, App.DERIVED_NOTE states the rule in words. */
/* PEOPLE READ AS PEOPLE: the initial disc (17 Aug 2026, TRIM.md step 7).

   The design proof for the "monotonic" problem, on this one page. Every row
   leads with a disc carrying the person's initial in ink over a wash of their
   band colour, ringed in that colour, so the book reads as a row of faces with
   the temperature on them; the strip above is the same colours broken into
   people. Tokens only: the wash is the band colour mixed into the surface, the
   ring is the band colour, the letter is ink. No touches yet is a dashed ring
   with nothing inside: not a cold relationship, no relationship yet. Same type
   size as the rest of the page; the shape is what changed. */
.pdisc{
  display:inline-flex; align-items:center; justify-content:center; flex:none;
  width:30px; height:30px; border-radius:50%;
  font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:13px; line-height:1;
  color:var(--ink);
  background:color-mix(in srgb, var(--band, var(--ink-3)) 22%, var(--surface));
  box-shadow:inset 0 0 0 1.5px var(--band, var(--ink-3));
}
.pdisc.band-warm{--band:var(--warm)}
.pdisc.band-cooling{--band:var(--cooling)}
.pdisc.band-cold{--band:var(--cold)}
.pdisc.band-none{
  --band:var(--band-none);
  background:transparent; box-shadow:none;
  border:1.5px dashed var(--band-none); color:var(--ink-3);
}
.pdisc.lg{width:52px; height:52px; font-size:20px}

.badge.proposed{border-style:dashed}
.derived{
  text-decoration:underline dashed var(--ink-3);
  text-decoration-thickness:1px; text-underline-offset:3px;
}
.callout.derived{border-left-style:dashed; text-decoration:none}

/* ---------- cards ---------- */
/* align-items:start, the same rule .inbox-cards, .strat-cards and .wl-groups
   already carry: stretched to a shared row height, a card with less in it grows
   an empty pocket inside its own border, which reads as something that failed
   to load rather than as a card with less in it. */
.cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(min(280px, 100%),1fr)); gap:var(--gap-box); align-items:start}
/* Every card wears a trace of the room it is in.

   Kiana, 27 Jul 2026: "in all pages you don't use the assigned color enough,
   that's also the reason why everything looks dull". The count backed her up.
   Six accents were defined, one per room, and outside a kicker, a primary
   button and a nav dot they were doing nothing at all: 2% of the characters on
   the dashboard carried --page, against 52% in the dullest grey.

   The fix is not louder copy, it is that the furniture belongs somewhere. A
   sixth of the accent mixed into the border is not a coloured box, it is a
   grey box that is faintly blue in the book and faintly orange in the money, and
   the difference is felt rather than read. */
/* THE ROOM'S COLOUR ON THE CARD'S EDGE.

   Borrowed from the sibling app, which Kiana reads as more coherent and more
   navigable than this one, and this is the move that does most of that work:
   every card carries a short accent rule on its leading edge. It costs three
   pixels, it needs no new token, and it gives a page of grey boxes a spine
   you can find from across the room, which for readers who navigate by colour
   before they navigate by words is the difference between scanning and
   reading.

   Deliberately restrained against the sibling: a rule at the top edge rather
   than a full-height left bar, so a card in a column still reads as a card
   and not as a quote block, and at 55% so it never competes with the data
   inside. The bar is a pseudo-element clipped by the card's own radius, so
   nothing about the card's box model moves and no layout shifts. */
.card{
  background:var(--surface);
  border:1px solid color-mix(in srgb, var(--page,var(--brand)) 17%, var(--line));
  border-radius:var(--radius);
  padding:var(--pad-card); box-shadow:var(--shadow);
  position:relative; overflow:hidden;
  transition:border-color var(--t), transform .2s cubic-bezier(.2,.7,.2,1);
  /* A card is a box that fits its column, never one that widens it. As a grid
     item its min-width defaults to auto, meaning "at least my longest
     unbreakable content", so one long word or one nowrap row inside any card
     in any of this app's card grids pushed its track past the viewport and the
     page scrolled sideways. Measured at 320px on Settings and at 375px on
     Drafts. A card may scroll inside itself; the page may not. */
  min-width:0;
}
/* the leading edge itself. Every card gets it; a card that sets its own
   --accent (the spine cards, the error card) keeps that colour instead. */
.card::before{
  content:""; position:absolute; inset:0 0 auto 0; height:3px;
  background:var(--accent, var(--page, var(--brand)));
  opacity:.55; pointer-events:none;
  transition:opacity var(--t);
}
.card.link:hover::before, a.card:hover::before{opacity:.95}
/* a nested card would draw a second rule inside the first: one per level */
.card .card::before{display:none}

.card.link, a.card{display:block; text-decoration:none; color:inherit; cursor:pointer}
.card.link:hover, a.card:hover{
  border-color:color-mix(in srgb, var(--page,var(--brand)) 40%, var(--line));
  transform:translateY(-2px);
}
.card h3{margin-bottom:6px}
.card p{color:var(--ink-2); font-size:15px}
.card p:last-child{margin-bottom:0}
.card .card-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:10px}
.card.wide{grid-column:1/-1}

/* two-panel layouts (drafts compose/result, detail columns) */
.split{display:grid; grid-template-columns:1fr 1fr; gap:var(--gap-box); align-items:start}
@media(max-width:860px){ .split{grid-template-columns:1fr} }
/* A GRID ITEM DOES NOT SHRINK BELOW ITS CONTENT UNLESS YOU SAY SO.

   min-width on a grid item defaults to auto, which means "at least as wide as
   my longest unbreakable content". One card holding one long line of
   white-space:nowrap therefore widened its track past the viewport and the
   whole PAGE scrolled sideways: measured at 375px, the Drafts voice-samples
   card forced the document to 647px, and no other view in the app did that.
   The card is where a horizontal scrollbar is allowed to live, never the page.

   This is the general fix, not a patch for one card: every .split item can hold
   a table, a long title or a pasted URL, and every one of them would have done
   the same thing. */
.split > *{min-width:0}

/* quiet row lists inside cards and panels */
.list{display:flex; flex-direction:column}
.list > *{padding:var(--s3) 2px; border-bottom:1px solid var(--line-soft)}
.list > *:last-child{border-bottom:none; padding-bottom:0}
/* The first row used to take padding-top:2px, which pinned it against the
   heading above while every other row sat a comfortable 12px from its
   neighbour: the list opened tight and then relaxed, which is the crowding
   read backwards. It keeps its padding now, and the LAST row gives up its
   bottom padding instead, so the block still ends flush with the card. */
.list > *:first-child{padding-top:0}

/* callout: one-line honesty and warnings */
.callout{
  border:1px solid var(--line); border-left:3px solid var(--page,var(--brand));
  border-radius:var(--radius-s);
  background:linear-gradient(180deg, var(--surface), var(--bg-2));
  padding:12px 16px; margin:14px 0;
  font-size:13.5px; color:var(--ink-2);
}
.callout.warn{border-left-color:var(--warn)}
.callout.danger{border-left-color:var(--danger)}
.callout b{color:var(--ink); font-weight:500}

/* pure-CSS horizontal bar breakdowns (revenue, career timeline) */
.barrow{display:grid; grid-template-columns:minmax(90px,170px) 1fr auto; gap:12px; align-items:center; padding:5px 0}
.barrow .lbl{font-size:13.5px; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.barrow .val{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:12px; color:var(--ink-2)}
/* NO TRACK BEHIND THE FILL (27 Aug 2026).

   Kiana: "it looks like the bar is filled at this number but this is not the
   maximum."

   She is right, and it was a lie of form. These bars are scaled to the largest
   value in their own set, not to any ceiling, so a dark track running to the
   right edge drew a capacity that does not exist: CHF 480 looked like 70% of
   something, when the something was only "the biggest row here". Every row also
   prints its real figure, so nothing is lost by removing the track and the
   remaining length stops making a claim.

   The box keeps its height and its clip, so the fill still rounds correctly and
   the grid column still reserves the same space. */
.bar{height:8px; background:none; border-radius:99px; overflow:hidden}
/* THE FILL FADES ALONG ITS OWN LENGTH.

   Kiana: "i think the bar with the new orange can be nicer with fading to
   white. not these harsh colors you always implemented."

   A flat slab of accent at 85% is the loudest thing on a card whose job is to
   let five amounts be compared, and five of them stacked is five slabs. The
   fill now runs from its own colour at the left, where the bar starts and the
   eye lands, to a lighter mix of that same colour toward --ink at the right,
   so it lifts as it travels instead of stopping dead.

   --ink, not #fff: the app's off-white is the lightest thing in the palette
   and a literal white would be the one colour on screen that belongs to
   nothing. The hue is unchanged along the whole run, so a bar still reads as
   its own room's colour; only its weight moves. `background-size` is pinned to
   the element so the gradient spans the FILL rather than the track, which
   keeps the fade proportional to the value instead of to the card. */
.bar i{
  display:block; height:100%; border-radius:99px; min-width:2px;
  --fill:var(--page,var(--brand));
  background:linear-gradient(90deg,
    var(--fill) 0%,
    color-mix(in srgb, var(--fill) 62%, var(--ink)) 100%);
  background-size:100% 100%;
}

/* done things go quiet, not away */
.strike{text-decoration:line-through; color:var(--ink-3)}

/* ---------- stats ---------- */
/* A GRID, SO EVERY TILE IS ONE WIDTH.

   As a wrapping flex row with flex:1, each LINE shared out its own leftover
   space, so eight tiles came out 154px, 154, 154, 154, 154, 154, 503, 503: the
   two that happened to wrap were three times the width of their siblings, same
   component, same row, one card. A grid gives every tile the same column
   whatever line it lands on. */
.statline{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(128px, 1fr));
  gap:var(--s5); margin-bottom:var(--gap-box);
}
.stat{
  min-width:0; /* the grid sizes the tile; this only lets long values wrap */
  /* every stat tile is a door now (dashboard and monthly report both), and a
     tile is a box, not a sentence: no underline, and the whole tile lifts on
     hover the way .card.link does */
  text-decoration:none; display:block;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  padding:var(--s4);
}
a.stat:hover{border-color:color-mix(in srgb, var(--page,var(--brand)) 40%, var(--line))}
/* On a phone six tiles packed three-across left the longest label ("earned in
   chf") wrapping while its neighbours did not, so the row sat ragged. Two
   across gives every label one line and the same tile height. */
@media(max-width:560px){
  .statline{grid-template-columns:repeat(2, minmax(0, 1fr))}
}
/* THE FIGURE WEARS THE ROOM IT OPENS.

   A tile is a door, and every tile now carries the accent of the room behind
   it (App.wearRoom). That accent was reaching only the hover border and the
   focus ring, so at rest six tiles read as one grey wall and the reader had to
   hover each one to learn where it went. The figure is the largest thing on
   the tile and the thing being counted, so it is what should say which room
   the count belongs to: contacts blue, nights green, tracks yellow, money orange.

   --page falls back to --brand, so a tile on a room with no accent of its own
   (Settings) still prints in the signature purple rather than in nothing. */
.stat b{
  display:block; font-family:"Space Grotesk",sans-serif; font-weight:600;
  font-size:26px; letter-spacing:-.01em; line-height:1.15;
  color:var(--page, var(--brand));
}
/* a tile with nothing to report stops competing with the ones that do */
.stat.at-rest b{color:var(--ink-3)}
.stat span{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px; letter-spacing:.07em; text-transform:uppercase; color:var(--ink-3)}

/* ---------- table ---------- */
.tablewrap{overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface)}
/* STICKY HEADERS, WHERE THEY CAN BE TRUE.

   th{position:sticky} has been in this file from the start and has never once
   engaged: overflow-x:auto makes the wrap the th's scroll container, the wrap
   is always exactly as tall as its table, so there is nothing to pin against.
   A 31,000px gig list scrolled headerless while the code claimed otherwise.

   The page must never scroll sideways, so the wrap keeps its overflow on
   narrow screens where tables genuinely outgrow it. On a wide screen the one
   table long enough to need a pinned header, the career gigs list, fits its
   column, so THERE the wrap steps aside, the page becomes the scroll
   container, and the header pins below the topbar for real. Scoped to the one
   view measured to fit rather than granted app-wide on hope. */
@media (min-width: 1240px){
  .view-career .tablewrap{overflow-x:visible}
  .view-career .table th{top:var(--topbar-h)}
}
/* At phone widths the nowrap action columns hold their width and the name
   column used to crush into one-word-per-line towers. Names and dates hold one
   line and ellipsize instead; the wrap already pans sideways for the rest. */
@media (max-width: 560px){
  .table td:first-child{white-space:nowrap}
  /* a code (ISRC, ISWC, SUISA number) broke at its hyphens into a four-line
     tower at phone width; the wrap already pans, so codes hold one line */
  .table td .mono, .table td.mono{white-space:nowrap}
  .view-career .table td:nth-child(2){
    max-width:170px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
}
.table{width:100%; border-collapse:collapse; font-size:13.5px}
.table th{
  position:sticky; top:0; z-index:1;
  text-align:left; padding:10px 14px;
  background:var(--surface-2);
  border-bottom:1px solid var(--line);
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; font-weight:500; letter-spacing:.09em; text-transform:uppercase;
  color:var(--ink-3); white-space:nowrap;
}
.table td{
  padding:13px 14px; border-bottom:1px solid var(--line-soft);
  color:var(--ink-2); font-variant-numeric:tabular-nums; vertical-align:middle;
}
.table tbody tr:last-child td{border-bottom:none}
.table tbody tr{transition:background var(--t)}
.table tbody tr:hover{background:color-mix(in srgb, var(--surface-2) 62%, transparent)}
.table tr.click{cursor:pointer}
.table td b, .table td .strong{color:var(--ink); font-weight:500}
.table td a{text-decoration:none; color:var(--ink)}
.table td a:hover{color:var(--page,var(--brand))}
.table .mono, .table td.mono{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:12px}
.table .r{text-align:right}

/* ---------- forms ---------- */
.field{display:flex; flex-direction:column; gap:6px; margin:0 0 14px; min-width:0}
.field label{
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.08em; text-transform:uppercase;
  /* ink-2, not ink-3: form labels are essential text and must clear 4.5:1 */
  color:var(--ink-2);
}
.field input, .field select, .field textarea,
.fx-row input, .fx-row select,
.modal-panel input, .modal-panel select, .modal-panel textarea{
  width:100%;
  background:var(--bg-2); border:1px solid var(--line); color:var(--ink);
  border-radius:var(--radius-s); padding:9px 12px;
  font-family:inherit; font-size:13.5px; line-height:1.45;
  transition:border-color var(--t), box-shadow var(--t);
}
.field input:focus, .field select:focus, .field textarea:focus,
.modal-panel input:focus, .modal-panel select:focus, .modal-panel textarea:focus{
  outline:none;
  border-color:var(--page,var(--brand));
  box-shadow:0 0 0 3px color-mix(in srgb, var(--page,var(--brand)) 15%, transparent);
}
.field textarea{min-height:92px; resize:vertical}
.field .hint{font-size:12px; color:var(--ink-3); line-height:1.5}
.field input::placeholder, .field textarea::placeholder{color:var(--ink-3)}
.form-grid{display:grid; grid-template-columns:1fr 1fr; gap:0 16px}
.form-grid .full{grid-column:1/-1}
.form-err{color:var(--danger); font-size:13.5px; margin:2px 0 10px}
@media(max-width:560px){ .form-grid{grid-template-columns:1fr} }

/* search pill */
.searchwrap{position:relative; display:inline-flex; align-items:center}
/* the magnifier is App.icon("search", { size: 14 }); colour rides on the
   wrapper rule so the icon keeps inheriting currentColor */
.searchwrap svg{position:absolute; left:12px; width:14px; height:14px; color:var(--ink-3); stroke:currentColor; fill:none; pointer-events:none}
.searchwrap input{
  width:230px; max-width:58vw;
  background:var(--surface); border:1px solid var(--line); color:var(--ink);
  border-radius:99px; padding:8px 15px 8px 34px;
  font-family:inherit; font-size:13.5px;
  transition:border-color var(--t), box-shadow var(--t);
}
.searchwrap input:focus{
  outline:none; border-color:var(--page,var(--brand));
  box-shadow:0 0 0 3px color-mix(in srgb, var(--page,var(--brand)) 15%, transparent);
}
.searchwrap input::placeholder{color:var(--ink-3)}

/* ---------- badges + chips + tabs ---------- */
.badge{
  display:inline-flex; align-items:center; gap:6px;
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-2); background:var(--surface-2);
  border:1px solid var(--line); border-radius:99px; padding:2.5px 9px;
  white-space:nowrap;
}
.badge.accent{
  color:var(--page,var(--brand));
  border-color:color-mix(in srgb, var(--page,var(--brand)) 42%, var(--line));
  background:transparent;
}
.badge.ok{color:var(--ok); border-color:color-mix(in srgb, var(--ok) 42%, var(--line)); background:transparent}
.badge.warn{color:var(--warn); border-color:color-mix(in srgb, var(--warn) 42%, var(--line)); background:transparent}
.badge.danger{color:var(--danger); border-color:color-mix(in srgb, var(--danger) 42%, var(--line)); background:transparent}

.chip{
  display:inline-flex; align-items:center; gap:7px;
  font-family:inherit; font-size:12px; color:var(--ink-2);
  background:var(--surface); border:1px solid var(--line); border-radius:99px;
  padding:4px 12px; text-decoration:none; cursor:pointer;
  transition:border-color var(--t), color var(--t), background var(--t);
}
.chip:hover{border-color:var(--page,var(--brand)); color:var(--ink)}
.chip.on{
  border-color:var(--page,var(--brand)); color:var(--ink);
  background:color-mix(in srgb, var(--page,var(--brand)) 13%, transparent);
}
.chiprow{display:flex; gap:8px; flex-wrap:wrap}

/* member picker: toggleable team-member chips (App.memberPicker), reuses the
   chip look with an aria-pressed on-state so it reads as a real toggle */
.member-picker{display:flex; gap:8px; flex-wrap:wrap}
.member-chip{
  appearance:none;
  display:inline-flex; align-items:center; gap:7px;
  font-family:inherit; font-size:12px; color:var(--ink-2);
  background:var(--surface); border:1px solid var(--line); border-radius:99px;
  padding:4px 12px; cursor:pointer;
  transition:border-color var(--t), color var(--t), background var(--t);
}
.member-chip:hover{border-color:var(--page,var(--brand)); color:var(--ink)}
.member-chip[aria-pressed="true"]{
  border-color:var(--page,var(--brand)); color:var(--ink);
  background:color-mix(in srgb, var(--page,var(--brand)) 13%, transparent);
}

.tabs{display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 24px; overflow-x:auto; overflow-y:clip}
.tabs > *{
  appearance:none; background:none; border:none; cursor:pointer;
  border-bottom:2px solid transparent; margin-bottom:-1px;
  color:var(--ink-2); padding:9px 15px 10px;
  font-family:inherit; font-size:13.5px; font-weight:500; text-decoration:none; white-space:nowrap;
  transition:color var(--t), border-color var(--t);
}
.tabs > *:hover{color:var(--ink)}
.tabs > .active, .tabs > [aria-selected="true"]{color:var(--ink); border-bottom-color:var(--page,var(--brand))}

/* ---------- touch words ----------
   The closeness meter (bar + 0-100 score) lived here until 26 Jul 2026. It is
   words now: the facts with units, nothing to decode. Overdue reads through a
   calm warm tint on the words; the words already say it, so colour is never
   the only carrier. */
.touchwords{
  white-space:nowrap; vertical-align:middle;
  font-size:10.5px; color:var(--ink-3);
}
.touchwords.overdue{color:color-mix(in srgb, var(--warn) 62%, var(--ink-3))}
.touchwords.big{font-size:12px}

/* ---------- empty + loading + errors ---------- */
.empty{
  text-align:center; padding:52px 22px;
  border:1px dashed var(--line); border-radius:var(--radius);
  background:color-mix(in srgb, var(--surface) 45%, transparent);
  color:var(--ink-2);
}
/* the mark for the thing that is missing, at 28px, in the room's own accent.
   Colour goes on the wrapper so the svg keeps inheriting currentColor. */
.empty .glyph{
  color:color-mix(in srgb, var(--page,var(--brand)) 45%, transparent);
  margin-bottom:10px; user-select:none; line-height:1;
}
.empty p{margin:0 auto 16px; max-width:46ch; font-size:15px}
.empty p:last-child{margin-bottom:0}

.loading{
  display:flex; align-items:center; gap:11px;
  padding:46px 0; color:var(--ink-3);
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.1em; text-transform:uppercase;
}
.loading::before{
  content:""; width:13px; height:13px; flex:none; border-radius:99px;
  border:2px solid var(--line); border-top-color:var(--page,var(--brand));
  animation:spin .9s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}

/* The rail too, not just the border. .card::before paints from
   var(--accent, var(--page, var(--brand))), and nothing in the app had ever
   set --accent, so an error card drew a 3px rule in the ROOM's colour above a
   red border and a red kicker. Under the old copper the two at least read as
   one warm family; purple against --danger is 62.9 dE apart and simply looks
   like two unrelated things. --accent rather than --page on purpose: exactly
   one declaration reads it, so the rail changes and no descendant control
   does. This also gives the documented escape hatch its first real user. */
.error-card{--accent:var(--danger); border-color:color-mix(in srgb, var(--danger) 40%, var(--line))}
.error-card .kicker{color:var(--danger)}
.error-card .errmsg{
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:12px; color:var(--ink-2);
  background:var(--bg-2); border:1px solid var(--line-soft); border-radius:var(--radius-s);
  padding:10px 13px; margin:12px 0 16px; overflow-x:auto;
}

/* ---------- modal ---------- */
.modal-overlay{
  position:fixed; inset:0; z-index:100;
  display:flex; align-items:flex-start; justify-content:center;
  padding:7vh 18px 24px; overflow-y:auto;
  background:color-mix(in srgb, var(--bg) 68%, transparent);
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  animation:overlay-in .16s ease both;
}
@keyframes overlay-in{from{opacity:0} to{opacity:1}}
/* A DIALOG FITS THE WINDOW, AND ITS BUTTON IS ALWAYS ON SCREEN (27 Aug 2026).

   "Add a track" is 939px tall. In an 800px window that put "Add track" 188px
   below the fold, with nothing to suggest it was there: the panel had no
   max-height, so only the overlay behind it scrolled, and a reader who filled
   the form in had no visible way to save it.

   The panel is now capped to the window, the BODY scrolls inside it, and the
   head and the actions stay put at top and bottom. The scroll happens where
   the fields are, which is the only place it means anything, and the primary
   button is on screen at every height. */
.modal-panel{
  width:min(560px,100%);
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow); padding:22px 22px 20px;
  animation:panel-in .22s cubic-bezier(.2,.7,.2,1) both;
  max-height:calc(100vh - 80px);
  display:flex; flex-direction:column;
}
.modal-panel > .modal-body{
  /* min-height:0 is the load-bearing half. A flex child will not shrink below
     its own content without it, so the panel honoured max-height, the body
     kept its full height anyway, and the actions row was pushed straight out
     of the bottom: capped panel, button still off screen. */
  flex:1 1 auto; min-height:0;
  overflow-y:auto;
  /* room for the focus ring on the first and last field inside the scroller */
  margin:0 -4px; padding:0 4px;
}
.modal-panel > .modal-head,
.modal-panel > .modal-actions{flex:none}
@keyframes panel-in{from{opacity:0; transform:translateY(14px)} to{opacity:1; transform:none}}
.modal-head{display:flex; align-items:center; justify-content:space-between; gap:14px; margin-bottom:16px}
.modal-head h2{font-size:20px}
.modal-x{
  appearance:none; background:none; border:none; cursor:pointer; flex:none;
  width:30px; height:30px; border-radius:7px; display:grid; place-items:center;
  color:var(--ink-3); font-size:13.5px; line-height:1;
  transition:background var(--t), color var(--t);
}
.modal-x:hover{background:var(--surface-2); color:var(--ink)}
.modal-body{min-width:0}
/* THE SAVE BUTTON STAYS ON SCREEN (27 Aug 2026).

   Each dialog builds its own actions row inside its form, so the row lives
   INSIDE the scrolling body rather than beside it: capping the panel just meant
   the buttons scrolled away with the fields. Sticky pins it to the bottom of
   whatever scrolls, which works wherever a dialog chooses to put it. The
   background is the panel's own, so fields cannot show through underneath, and
   the negative margin lets it span the body's padding. */
.modal-actions{
  display:flex; justify-content:flex-end; gap:10px; margin-top:20px;
  position:sticky; bottom:0;
  background:var(--surface);
  padding:12px 4px 0;
  margin-left:-4px; margin-right:-4px;
  border-top:1px solid var(--line-soft);
}

/* ---------- toast ---------- */
.toastroot{
  position:fixed; left:50%; bottom:26px; transform:translateX(-50%);
  z-index:220; display:flex; flex-direction:column; align-items:center; gap:8px;
  pointer-events:none;
}
.toast{
  max-width:min(92vw,460px);
  background:var(--surface-2); border:1px solid var(--line);
  border-left:3px solid var(--ok);
  color:var(--ink); border-radius:var(--radius-s);
  padding:9px 16px; font-size:13.5px; box-shadow:var(--shadow);
  animation:toast-in .22s cubic-bezier(.2,.7,.2,1) both;
  transition:opacity .35s ease, transform .35s ease;
}
/* a toast that carries an action is a row, not a sentence: the way back sits
   at the end of the line it belongs to and never wraps under it */
.toast.has-act{display:flex; align-items:center; gap:14px; padding-right:10px}
.toast-act{
  flex:none; font:inherit; font-weight:600; cursor:pointer;
  color:var(--page,var(--brand));
  background:none; border:1px solid currentColor; border-radius:var(--radius-s);
  padding:3px 10px;
}
.toast-act:hover{background:color-mix(in srgb, var(--page,var(--brand)) 14%, transparent)}
.toast.warn{border-left-color:var(--warn)}
.toast.danger{border-left-color:var(--danger)}
.toast.out{opacity:0; transform:translateY(8px)}
@keyframes toast-in{from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:none}}

/* ---------- identity overlay ---------- */
.identity-overlay{
  position:fixed; inset:0; z-index:200;
  display:flex; align-items:center; justify-content:center; padding:20px;
  background:color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  overflow-y:auto;
}
.identity-panel{
  width:min(420px,100%);
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow); padding:28px 26px 24px;
  animation:panel-in .25s cubic-bezier(.2,.7,.2,1) both;
}
.identity-panel .brandrow{display:flex; align-items:center; gap:10px; margin-bottom:22px}
.identity-panel .brandrow .word{font-family:"Space Grotesk",sans-serif; font-weight:600; font-size:17px}
.identity-panel .brandrow .ctx{font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px; letter-spacing:.08em; color:var(--ink-3)}
.identity-panel h2{font-size:20px; margin-bottom:8px}
.identity-panel .honest{font-size:13.5px; color:var(--ink-3); line-height:1.55; margin-bottom:18px}
/* --s4 for the same reason .statline takes it: 8px between bordered boxes
   holding 10px of padding is the spacing law backwards, and a stack of
   separate boxes read as one crowded field. 16px clears the padding on both
   axes and is on the scale, where 8px was not. */
.id-users{display:flex; flex-direction:column; gap:var(--s4); margin:0 0 6px}
.id-user{
  display:flex; align-items:center; gap:11px; width:100%; text-align:left;
  background:var(--bg-2); border:1px solid var(--line); border-radius:var(--radius-s);
  color:var(--ink); padding:10px 13px; font-family:inherit; font-size:13.5px; cursor:pointer;
  transition:border-color var(--t), background var(--t);
}
.id-user:hover{border-color:var(--brand); background:var(--surface-2)}
.id-user .role{margin-left:auto; font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3)}
.id-or{
  display:flex; align-items:center; gap:12px; margin:16px 0 14px;
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-3);
}
.id-or::before, .id-or::after{content:""; flex:1; height:1px; background:var(--line-soft)}

/* ---------- command palette ---------- */
/* above the page and open modals (100), below the identity gate (200) */
.palette-overlay{
  position:fixed; inset:0; z-index:150;
  display:flex; align-items:flex-start; justify-content:center;
  padding:12vh 18px 24px;
  background:color-mix(in srgb, var(--bg) 68%, transparent);
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  animation:overlay-in .16s ease both;
}
.palette-panel{
  width:min(600px,100%);
  display:flex; flex-direction:column;
  max-height:min(480px,76vh); overflow:hidden;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow);
  animation:panel-in .22s cubic-bezier(.2,.7,.2,1) both;
}
.palette-inputrow{
  display:flex; align-items:center; gap:11px; flex:none;
  padding:13px 16px;
  border-bottom:1px solid var(--line-soft);
}
.palette-inputrow svg{flex:none; color:var(--brand); stroke:currentColor; fill:none}
.palette-inputrow input{
  flex:1; min-width:0;
  background:none; border:none; outline:none;
  color:var(--ink); font-family:inherit; font-size:15px; line-height:1.4;
}
.palette-inputrow input::placeholder{color:var(--ink-3)}
/* the search closes by tap outside; on a touch screen the keyboard hints say
   nothing, so they go and a plain close button takes their place */
.palette-close{display:none}
@media(pointer:coarse){
  .palette-inputrow kbd{display:none}
  .palette-close{
    display:inline-flex; align-items:center; justify-content:center; flex:none;
    width:36px; height:36px; border-radius:8px; border:1px solid var(--line);
    background:var(--surface-2); color:var(--ink-2); cursor:pointer;
  }
}
.palette-inputrow kbd{
  flex:none;
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-3); background:var(--surface-2);
  border:1px solid var(--line); border-radius:5px; padding:2px 6px;
}
.palette-results{overflow-y:auto; padding:6px 8px 10px}
.palette-kicker{
  font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.13em; text-transform:uppercase;
  color:var(--brand); margin:12px 10px 4px;
}
.palette-group:first-child .palette-kicker{margin-top:6px}
.palette-row{
  display:flex; align-items:baseline; gap:12px;
  padding:8px 10px; border-radius:var(--radius-s); cursor:pointer;
}
.palette-row .p-name{
  color:var(--ink); font-size:15px; min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.palette-row .p-sub{
  margin-left:auto; flex:none; max-width:55%;
  color:var(--ink-3); font-size:12px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.palette-row.active{
  background:var(--surface-2);
  box-shadow:inset 2px 0 0 var(--brand);
}
.palette-row.active .p-sub{color:var(--ink-2)}
.palette-note{padding:26px 20px; text-align:center; color:var(--ink-3); font-size:13.5px}
.palette-note.err{color:var(--danger)}

/* ---------- responsive ---------- */
@media(max-width:900px){
  .shell{grid-template-columns:1fr}
  .sidebar{
    position:fixed; left:0; top:var(--topbar-h); bottom:0; z-index:80;
    width:262px; max-width:82vw; height:auto;
    background:var(--bg-2); border-right:1px solid var(--line);
    transform:translateX(-105%); transition:transform .24s cubic-bezier(.2,.7,.2,1);
    box-shadow:var(--shadow);
  }
  .sidebar.open{transform:none}
  .menu-btn{display:inline-flex}
}
@media(max-width:560px){
  main{padding-left:16px; padding-right:16px}
  .viewhead{padding-top:30px}
  .viewhead .actions{width:100%; padding-bottom:0}
  /* THIS BLOCK USED TO UNDO THE ONE ABOVE IT.

     Two bare `.stat` rules sit in two separate max-width:560px blocks in this
     one file, at identical specificity, so the later one wins and the
     documented two-across fix a few hundred lines up never applied to a single
     tile in the app. Measured on Revenue at 375px: three tiles at 108px, "top
     society · CHF" wrapping to two lines and "live fees" to one, which is
     exactly the ragged row that comment says was fixed. The min-width is gone;
     the calc governs again, and the tiles that need it keep the smaller type
     and tighter padding.

     The gap went with it. 9px between tiles against 14px of padding inside them
     is the spacing law backwards, and it made a row of separate bordered boxes
     read as one crowded field. --s4 is 16px, which clears the padding, and it
     is on the scale where 9px never was. */
  .statline{gap:var(--s4)}
  .stat{padding:12px 14px 10px}
  .stat b{font-size:20px}
  .modal-overlay{padding:16px 12px}
  .searchwrap input{width:170px}
  .palette-overlay{padding:8vh 10px 16px}
}

@media(prefers-reduced-motion:reduce){
  *{animation:none !important; transition:none !important; scroll-behavior:auto !important}
}

/* ---------- touch ----------
   A control you tap with a thumb needs a bigger target than one you click with
   a cursor. Apple and Google both land on about 44px. These rules fire only on
   a coarse pointer, so the desktop density every other rule was tuned for is
   untouched. Measured before writing them: the career table alone carried about
   1200 controls under 36px tall.

   Dense tables get the hit area WITHOUT the bulk. Growing a row action to 44px
   would add a centimetre to every one of 442 rows, so the button keeps its size
   and an invisible pseudo-element carries the touch area. The vertical inset is
   deliberately smaller than the horizontal one: rows sit close together, so a
   generous vertical target would overlap the row above and turn a tap on Edit
   into a tap on the wrong gig. */
@media (pointer: coarse) {
  .btn{padding:11px 16px}
  .btn.small{padding:9px 14px; font-size:13.5px}
  .chip{padding:12px 15px}
  .member-chip{padding:8px 14px}
  .tool-btn, .menu-btn{width:44px; height:44px}
  .id-chip{padding:6px 15px 6px 6px}
  .sidebar a{padding:11px 10px}
  .field input, .field select, .field textarea{min-height:44px}

  /* THE FOUR THIS BLOCK STILL MISSED (4 Sep 2026), found by measuring the
     screens built the same day rather than by reading the rule.

     The line above is scoped to .field, which is most of a form and not all of
     one. Everything below sits outside a .field and was left at its mouse
     size:

       .btn                41px   the tall block button on a sign-up form
       .linkish            30px   "I already have an account", a real control
       select              19px   the money dropdown on the team row
       checkbox            13px   the admin box beside it

     A 19px dropdown carrying "who sees the money" is the worst of those. The
     first three grow, because they sit alone on their line and have the room.
     The checkbox does not: it grows a hit area through its label instead, so
     the box a person sees stays the size the design intends. */
  .btn{min-height:44px}
  .linkish{
    display:inline-flex; align-items:center;
    min-height:44px;
  }
  select{min-height:44px}
  /* the label already wraps the box, so it is the target and no pseudo-element
     is needed to make one */
  .team-admin{min-height:44px}
  .team-admin input{width:22px; height:22px}
  /* The way out of every dialog in the app, at 30px. It keeps its drawn size,
     because a close cross that suddenly fills a corner looks broken; the hit
     area grows underneath it instead, the way the row actions above do. */
  .modal-x{position:relative}
  .modal-x::after{
    content:""; position:absolute; top:50%; left:50%;
    width:44px; height:44px; transform:translate(-50%,-50%);
  }

  /* row actions: real hit area, unchanged footprint */
  .table td .btn.small, .table td a.plink, .list .acts .btn.small{
    position:relative;
  }
  .table td .btn.small::after, .table td a.plink::after, .list .acts .btn.small::after{
    content:""; position:absolute; left:-6px; right:-6px; top:-7px; bottom:-7px;
  }
  /* and more room between adjacent actions so the enlarged areas cannot overlap */
  .table td .acts, .list .acts{gap:14px}

  /* THE FOUR THIS BLOCK HAD NOT REACHED (27 Aug 2026).

     Measured under an emulated touch pointer rather than a mouse, which is
     why they were missed: every rule in this block is invisible at a fine
     pointer, so a desktop audit reports the untouched sizes and finds nothing
     to praise. On a coarse pointer the row actions already came out at 52px
     and the tool buttons at 44. These four did not:

       .chip 41   .btn.small (outside tables) 38   .id-chip 37   help dot 32

     Same technique as the row actions above, for the same reason: the visible
     control keeps its size and an invisible pseudo-element carries the touch
     area, so nothing in the layout moves. The insets are the smallest that
     reach 44, because every pixel past that is a pixel of somebody else's
     button. */
  .chip, .id-chip, .btn.small{position:relative}
  .chip::after, .id-chip::after, .btn.small::after{
    content:""; position:absolute; left:0; right:0;
    top:50%; height:44px; transform:translateY(-50%);
  }
  /* the row actions already have their own, tighter, vertical inset above:
     rows sit close together and 44px there would reach the row above */
  .table td .btn.small::after, .list .acts .btn.small::after{
    top:-7px; bottom:-7px; height:auto; transform:none;
  }
}

/* ---------- rows and charts that are doors ---------- */
.rowdoor{cursor:pointer; border-radius:8px}
/* --surface-1 alone is a 1.08:1 step, under the file's own visibility
   threshold; the dashboard fixed it with an accent rail and kept the fix to
   itself. The rail is the base rule now, so a report door answers hover the
   same way a dashboard door does. */
.rowdoor:hover{
  background:var(--surface-1);
  box-shadow:inset 2px 0 0 var(--page,var(--brand));
}
.rowdoor:focus-visible{outline:2px solid var(--page,var(--brand)); outline-offset:2px}

/* A DOOR HAS TO LOOK LIKE ONE BEFORE YOU TOUCH IT.

   Seven row types in this app open something when you press them. Three of
   them hold a real link, so the name is already underlined in the page accent
   and the row reads as a door at rest. The other four hold no link at all:
   the upcoming-gig row and the wishlist row on the dashboard, and both roll-up
   rows in the monthly report. At rest those four were identical to a row that
   does nothing, and the only thing that said otherwise was a title attribute
   and a background that appears on hover.

   That is Kiana's second complaint word for word: "it feels like you need a
   walkthrough to even know what else is clickable". A hover-only affordance is
   no affordance for this audience, and it is nothing at all on a touch screen.

   So a doored row with no link inside it underlines its own name, in the same
   ink and the same accent as every other in-app link. No new vocabulary: the
   reader has already learnt that this underline means "this opens something".
   :has() is already load-bearing in this file, so it costs nothing new. */
.rowdoor:not(:has(a)) b{
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-decoration-color:color-mix(in srgb, var(--page,var(--brand)) 34%, transparent);
  text-underline-offset:3px;
}
.rowdoor:not(:has(a)):hover b{
  text-decoration-color:color-mix(in srgb, var(--page,var(--brand)) 72%, transparent);
}
.strip-doors .ch-seg, .strip-doors .ch-key, .strip-doors .ch-single{cursor:pointer}
.strip-doors .ch-seg:hover{filter:brightness(1.25)}
.strip-doors .ch-key:hover{color:var(--ink)}

/* ---------- click the fact to change it ---------- */
.editable{display:inline-flex; align-items:center; vertical-align:middle}
/* THE DASHED LAW, applied to the control itself rather than to one child.

   An inline-editable value used to get its dashed underline from an optional
   `.inline-fact` span inside it. On a contact page that meant one of four
   editable controls looked editable: the rhythm had the dash, while the role,
   the city and the whole notes block had nothing at all and disclosed
   themselves only through a `title` that said "Click to ...", which a phone
   never shows. Same story in the catalog, where the ISRC, ISWC and SUISA cells
   are the entire job of the page.

   The dash lives on `.editable-btn` now, so every editable value in the app
   wears it whether or not anyone remembered to wrap the text. */
.editable-btn{
  all:unset; cursor:pointer; display:inline-flex; align-items:center;
  border-radius:6px;
  border-bottom:1px dashed color-mix(in srgb, var(--page,var(--brand)) 42%, transparent);
}
.editable-btn:hover{border-bottom-color:var(--page,var(--brand))}
/* a notes-sized editor is a field, not a word: the dash runs its whole width
   and sits away from the last line of text */
.editable-block .editable-btn{padding-bottom:4px}
/* the inner span keeps its own dash for the one case it still marks: a fact
   inside a sentence. Two dashes stacked would read as an underline bug. */
.editable-btn .inline-fact{border-bottom:none}

/* A pill that is editable dashes its OWN outline instead of growing a second
   line inside itself. The badge and the chip already draw a border; a dashed
   underline stacked within one reads as a rendering fault rather than as an
   invitation, and the dashed law is about the edge that is already there. */
/* AND THE DASH IS ACCENTED, WHICH IS WHAT SEPARATES IT FROM THE OTHER DASH.

   The app runs two dashed languages. One says the app worked this out: the
   proposed badge, the estimate frame, the derived edges on the graph, the
   median hairline. The other says you can change this. Both are useful and
   both are pervasive, so retiring either would cost more than it fixed.

   They only ever collided in one shape, a dashed pill border, where a
   proposed badge and an editable badge looked identical. So the two languages
   split by COLOUR rather than by shape, which is a distinction the editable
   underline was already making on its own: an accent dash means you can
   change it, a neutral dash means the app worked it out. The derived law
   keeps every mark it had, and the editable signal gets stronger, which is
   the right way round for readers who need to see what is actionable. */
.editable-btn:has(> .badge), .editable-btn:has(> .chip){border-bottom:none}
.editable-btn > .badge, .editable-btn > .chip{
  border-style:dashed;
  border-color:color-mix(in srgb, var(--page,var(--brand)) 42%, transparent);
}
.editable-btn:hover > .badge, .editable-btn:hover > .chip{border-color:var(--page,var(--brand))}
.editable-btn:hover{background:var(--surface-1)}
.editable-btn:focus-visible{outline:2px solid var(--page,var(--brand)); outline-offset:2px}
.editable-ctl{
  font:inherit; color:var(--ink);
  background:var(--surface-2); border:1px solid var(--page,var(--brand));
  border-radius:7px; padding:2px 8px; max-width:220px;
}
.editable-notes{display:block; width:100%; max-width:none; min-height:90px; padding:8px 10px}
.badge.ghost, .chip.ghost{opacity:.55; border-style:dashed}
.inline-fact{
  border-bottom:1px dashed var(--page,var(--brand));
  cursor:pointer;
}
/* the block form: notes-sized editors take their own line */
.editable-block{display:block; width:100%}
.editable-block .editable-btn{display:block; width:100%; text-align:left}
/* a chip that leaves the app, beside the one that edits the address */
.chip-out{display:inline-flex; align-items:center; gap:5px}
/* Inside a dense table the control must not shove the row wider, and the
   BUTTON must fill its cell. An empty society number renders as a single dash,
   so the tap target was the width of that dash: 8px by 22px on a phone, for
   the one job this table exists to make easy (78 works, two numbers each).
   Filling the cell gives a real target without a pixel of visual change, and
   without the overlapping hit areas that expanding outwards would create
   between three adjacent editors in one row. */
td .editable-ctl{max-width:150px; padding:1px 6px}
td .editable{display:block; width:100%}
td .editable-btn{
  display:flex; align-items:center;
  width:100%; min-height:32px;
}

/* The second line of a view head: the definition, the caveat, the where-things-
   come-from. A lede is one line and answers "what is this page"; anything that
   answers "and how does it work" sits under it, quieter and narrower, so the
   heading block still reads in one glance. Introduced 27 Jul 2026 when the
   inbox lede had reached 440 characters and the catalog lede had become a
   glossary. */
.viewhead .lede-more{
  color:var(--ink-3); margin:6px 0 0; max-width:70ch;
  font-size:13.5px; line-height:1.55;
}

/* A LINE BETWEEN BLOCKS, RUNNING INTO THE MARGIN (27 Aug 2026).

   Kiana: "i like to have some kind of slightly transparent line between all
   pages so it's not cramped up on top of each other. we have enough space on
   the left side."

   Sections sat straight on top of one another with only their own padding
   between them. A view drops one of these between blocks that are not already
   carded. It is a fraction of --line, so it separates without reading as an
   edge, and it runs left past the content into the page's own margin, which is
   what makes it a divider between two things rather than a border on one. */
.viewsep{
  height:1px;
  border:0;
  margin:34px 0 30px -24px;
  background:color-mix(in srgb, var(--line) 42%, transparent);
}

/* ---------------------------------------------------------------- the door
   Added 3 September 2026, when the platform grew real accounts. Three things
   are new on screen: the hyphen in the wordmark, the name of the artist whose
   book is open, and the account panel behind the chip. Everything here reads
   the tokens already declared in :root; a literal hex outside :root is a bug
   in this codebase and that has not changed. */

/* The hyphen is a design element, not punctuation: it wears the structural
   line colour so the wordmark reads as one word with a beat inside it. Scoped
   under .brand .word because .dash is also a dashboard container class. */
.brand .word .dash{color:var(--line); font-weight:500}

/* Whose book is open. It sits after the wordmark, quiet, and it is the one
   place a manager holding two artists can check which one they are reading. */
.ws-label{
  display:inline-flex; align-items:center; gap:8px; margin-left:2px;
  padding-left:12px; border-left:1px solid var(--line);
  min-width:0;
}
.ws-label .ws-name{
  font-family:"Space Grotesk",sans-serif; font-size:13px; color:var(--ink-2);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:22ch;
}
@media(max-width:720px){ .ws-label{display:none} }

/* The account panel, opened from the chip. */
.account-panel{display:flex; flex-direction:column; gap:var(--gap-box)}
.account-who{display:flex; align-items:center; gap:12px}
.account-who .ava.lg{
  width:38px; height:38px; border-radius:50%; display:grid; place-items:center;
  background:var(--surface-2); color:var(--ink-2);
  font-family:"Space Grotesk",sans-serif; font-size:16px; text-transform:uppercase;
}
.account-name{font-weight:600}
.account-email{color:var(--ink-3); font-size:13px}
.account-h{
  font-family:"JetBrains Mono",ui-monospace,monospace; font-size:10.5px;
  letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3);
  margin:0 0 2px;
}

/* The artists this account may open. The one being read is a row that cannot
   be pressed, because pressing it would do nothing and a dead control that
   looks alive is worse than a label. */
.ws-list{display:flex; flex-direction:column; gap:6px}
.ws-row{
  display:flex; align-items:baseline; gap:10px; width:100%; text-align:left;
  padding:10px 12px; border:1px solid var(--line); border-radius:var(--radius-s);
  background:var(--surface); color:var(--ink); cursor:pointer;
  transition:border-color var(--t), color var(--t);
}
.ws-row:hover:not(:disabled){border-color:var(--brand)}
.ws-row:disabled{cursor:default; opacity:1; border-color:var(--brand)}
.ws-row .ws-artist{font-weight:600}
.ws-row .ws-role{
  margin-left:auto; font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3);
}

/* An invitation link, shown once, meant to be copied and handed over. */
.invite-out{display:flex; flex-direction:column; gap:8px; margin-top:var(--gap-box)}
.invite-link{
  width:100%; font-family:"JetBrains Mono",ui-monospace,monospace; font-size:12px;
  padding:9px 11px; border:1px solid var(--line); border-radius:var(--radius-s);
  background:var(--bg-2); color:var(--ink-2);
}

/* The team row gained two controls on 4 Sep 2026: what a person may do with
   money, and whether they are an admin. Both sit inline with the name rather
   than in a second panel, because the question a reader has is "who can see
   what" and the answer should be one line per person. */
.settings-user select{max-width:22ch}
.team-admin{display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  font-size:13px; color:var(--ink-2)}
.team-admin input{margin:0}
