/* ============================================================================
   Event Radar — stylesheet (V1.17)
   Design direction: Editorial + Research Dashboard + Modern SaaS
   Light theme, warm paper background, hairline borders, one signal accent.

   Layout rules that drive most of what follows:
     §5  Navigation is a centred top bar; `.app` is a column, not a row.
     §1  Dashboard + Radar are "locked" pages: their own headers stay put and
         only the inner list / gantt body scrolls. `.view` is the single scroll
         container for every unlocked page (Database).
     §2  Content is centred with a max width, so nothing needs 90% browser zoom.
     §3  The database table uses fixed layout + percentage columns, so it never
         scrolls sideways, and its header row sticks below the sticky filter bar.

   V1.4 changes that shaped this file:
     §3  The event card is a fixed two-column frame (`.ev-body`) sized in vh, so
         both columns AND the notes editor are on screen without scrolling.
         `.why-box` is the boxed, icon-badged Why-it-matters block.
     §4  Notes are ordered newest-first and narrowed by a date range (`.range`).

   V1.5 changes:
     §1  `location.region` and `sourceNote` are gone from the data model, so no
         rule here mentions them any more.
     §4  `.dash-grip` is the dashboard splitter between Radar and Topic
         Distribution; the ratio rides in `--dash-ratio`.
     §5  The database filter bar is one row of three selectors and is sticky —
         and the table header had to be offset under it, see `--filters-h`.
     §6  Clicking a note card opens `.note-modal`.

   V1.6 changes:
     §1  The Dashboard masthead starts at the top of the page (`.is-dashboard`
         trimming `.view-inner`'s padding), the Radar panel has no head, and the
         splitter's default is 2.8:1.
         The route class is spelled from `route` (`is-` + `dashboard`), so these
         trims must use the full route name — `is-dash` silently matches nothing.
     §2  `.filter-summary` is deleted; the counts live in the Database header
         and `.cell-why` is the table's new last column.
     §3  The card's right column carries `.ev-desc` + `.ev-why` + `.ev-notes`;
         both writers are capped at two lines. `.seg-choice`, `.md-empty` and the
         two status fields' rules are gone or new accordingly.
     §4  `.nm-rule`, `.nm-ico` and `.nm-kicker` are gone. `.nm-head` centres its
         content and has no bottom hairline; the why-box centres as a box.
     §5  `.notes-grid` is 3 columns with `grid-auto-rows` at half the visible
         height, so two rows of six fit without zooming.

   V1.7 changes:
     §3  The event card is sized by its CONTENTS: `head + why (2 rows) +
         --ev-notes-h`. Description moved to the top of the left column, and the
         head is two centred lines (name / badges + categories, no id). Both
         centred heads share the `--mh-x-w` close-button spacer.
     §2  The why column is 340px — measured, not guessed (`probe-why-col.js`) —
         and the Database page is the one page allowed a wider `--content-max`.
     §5  `.notes-grid` shows two and a HALF rows: `(available - 32px) / 2.5`.
     §6  Warm paper: `.note-card`, `.md.md-preview` and the flat why-box all wear
         `--paper*`, so a note is the same material on the page and in the dialog.
     §7  `.g-grip` is the draggable seam that resizes the Gantt's label column;
         the width rides in `--g-label` (a px width, not a share).

   V1.9 changes:
     §1  The table gains a narrow flag column, Event Name and Why each take 21%,
         `.cell-org` clamps at three lines, and the header row is centred while
         only the two prose columns keep a left edge in the data rows.
     §2  The Database header is one sticky row of three parts (`.filters` now
         carries `.db-meta`: the counts and the two actions). Below the locked
         breakpoint only `.filter-row` goes into the drawer; the bar stays.
     §3  `.ev-follow` is the red pennant; `.seg-cats` is the card's category row,
         in the head beside `.seg-priority`. `.ev-title-row` centres flag + name
         as one unit. `.col-flag` is the table's flag column.
     §4  `.note-card.is-watch` is the white surface (S keeps the paper); `.nc-cats`
         moved into `.nc-head`; `.nc-top` is content-sized so nothing clips; the
         card's `.why-box.is-flat` is `fit-content` and centred.
     §5  `.dash-top` is a fixed 4:1 grid (timeline : Following) and `.dash-list`
         is the full-width event list under it. `.follow-card` is the panel's
         card. The old `.dash-body` / `.dash-grip` / `--dash-ratio` are gone.
         The `.dist*` bars went with Topic Distribution.

   V1.10 changes:
     §1  `.dash-top` is timeline : Topic (4:1, both stretched to one height);
         `.dash-bottom` is `.dash-events` | `.dash-grip` | `.dash-follow`, a
         draggable `--bottom-ratio` (3:1). `.dash-body` / `--dash-ratio` are
         gone; `.dist*` is back with Topic Distribution.
     §2  `.db-bar` is three `.db-mod` boxes (selectors / one statistic /
         stacked actions), sticky and flush under the nav. `.db-meta` is gone.
     §3  `.radar-head` is a `1fr auto 1fr` grid: summary, pager, view switcher —
         one row instead of two.
     §4  `.nm-body .why-box` is `fit-content` and centred; `.note-modal.is-watch`
         swaps the paper surfaces for white + grey.

   V1.11 changes:
     §1  `.dash-top` is a fixed 203px (65% of the 312px it measured) and its grid
         items are `min-height: 0`; the `.dist*` rows are tightened so all five
         umbrella bars still fit without scrolling.
     §2  The table's column order is flag · name · why · …; the organizer column
         is 10.8%.
     §3  `.filter-group` order is Following · Priority · Country · Category ·
         Date; the second `.segmented` is the Country split.

   V1.12 changes:
     §1  `.tl-head` is a `1fr auto 1fr` grid — the count alone on the left, the
         pager on the true centre line, the window selector on the right. Below
         820px it unstacks back into three stacked rows.
     §2  `--band-s` / `--band-watch` are the "priority bands": `.radar-name`
         and `.fc-name` sit on a wash instead of carrying a badge, and both are
         `flex: 0 1 auto` / `align-self: flex-start` so the wash hugs the words.
         The priority badge is off both Dashboard lists.
     §3  `.fc-loc` moved onto `.fc-head` (first line, right of the date);
         `.fc-why` is the body.
     §4  `table.data td.cell-flag` drops its padding and its clipping (the cell
         was painting the 26px button's overflow as a stray ".."); the website
         column is a fixed 74px holding one `.ico-link` glyph; the organizer
         column has no width at all, so it absorbs the leftover.

   V1.13 changes:
     §1  `.tl-bars` gets its height inline (a % of the track, scaled against the
         window's busiest day) and `.tl-bar` is `flex: 1 1 0`, so a busy day is a
         tall column of bars and a quiet one is a short column.
     §2  `.radar-name.band-*` / `.fc-name.band-*` use `--accent` (white ink) and
         `--watch` (`--watch-ink`), i.e. the timeline bar fill, not a new tint.
     §3  `.follow-list` is a three-row grid (`height: 100%`, rows = a third of
         the scrollport); `.fc-loc` is `margin-left: auto`; `.fc-desc` and
         `.fc-why` are the two graded prose fields.
     §4  `table.data td` is `vertical-align: middle` (the two prose columns stay
         `top`); `tr.is-ended` is greyed; `.db-count` rides the selectors' row;
         `.db-bar` is two columns. `[hidden]` is pinned to `display: none` so it
         can never again lose to `.btn { display: inline-flex }`.

   V1.14 changes:
     §1  `.dash-tl .tl-nav` holds the custom window's two date fields in the same
         centred seat the pager uses, so the head stays a three-seat grid.
     §2  `.radar-list` is a six-row grid (`height: 100%`, rows = a sixth of the
         scrollport) with `.radar-row { overflow: hidden }`.
     §3  `.follow-list` is a plain column again and the two `.fc-*` prose fields
         lost their clamps — the cards are content-sized.
     §4  `.th-flag` is the Following switch in the table's flag column
         (`th.col-flag` gets the same zero padding and no clipping as the body
         cells, for the same reason).

   V1.15 changes:
     §2  `.modal-foot.nm-foot` is a `1fr auto 1fr` grid — save line, `.nm-links`,
         `.nm-acts` — so the link glyphs sit on the card's centre line.

   V1.16 changes:
     §1  `.db-bar` is `3.4fr : 1fr` (the selectors box lands on 87% of the width
         it had alone); `.db-count` is gone; `.db-actions` is a 2×2 grid of four
         buttons; `.db-mod-filters .filter-row` is `space-between`.
     §2  `table.data thead th.col-flag` — the selector now carries the same
         weight as `table.data thead th`, or the zero-padding fix loses on source
         order and the header cell keeps painting ".." beside the pennant.
         Column plan gains a 30px `#` column at index 2 (`th.col-num`,
         `td.cell-num`), so every later `nth-child` shifts by one.
         `.cell-name .ef-name` is the in-place event-name field — transparent
         until hover or focus, so fifty rows do not read as a form.
     §3  On a phone: `.dash-bottom` becomes a flex column with `order` (Following
         above the events list), `.dash-events .panel` is one screen tall, and
         `.radar-list` divides it into four.
     §4  On a phone: `.g-scroll` scrolls both ways, `.g-label` and `.g-corner` are
         `position: sticky; left: 0`, and `.g-head`/`.g-row` get a `min-width` so
         the label column can be dragged without crushing the axis.
     §5  On a phone: `.notes-grid` is `grid-auto-rows: auto`, `.note-card` is
         `height: auto`, and every clamp inside the card is off.

   V1.17 changes:
     §1  `.data-ver` joins the topbar: the dataset version this browser is
         showing, as a pill beside the save line. Unlike `.saved` it is NOT
         hidden at the mobile breakpoint — the phone is precisely the device that
         has to be able to tell, at a glance, whether it has taken the release
         the PC just published. `.is-stale` turns it orange when the page shipped
         a newer dataset than the browser is carrying and the fetch did not
         happen (offline, or a deploy that landed mid-session).

   Removed over time (safe to delete if you see them in a stale file):
     V1.2  .stats / .stat-*            — the Overview band is gone
     V1.2  .tl-scroll                  — the timeline no longer pans sideways
     V1.2  .month / .cal-*             — the calendar grid became a Gantt
     V1.2  .detail-* / .official-*     — the detail page became a card
     V1.2  .list-editor / .list-item   — the research lists became one note box
     V1.2  .ro-text / .mobile-read     — read-only is now `readonly` on the input
     V1.3  .note-dot                   — the Notes page already says this
     V1.3  .ev-source                  — the source note left the card
     V1.4  .notes-sort                 — sorting is fixed at newest-first
     V1.4  .ev-lead                    — the description became an editable field
     V1.5  .nc-body / .nc-edit / .nc-toggle / .note-card.is-open
                                       — in-place expansion became `.note-modal`
     V1.5  .filter-row.is-tight        — the filter bar is a single row again
     V1.6  .filter-summary / .sum-right — folded into the Database header
     V1.6  .seg-choice                 — Content status / Data quality are gone
     V1.6  .nm-rule / .nm-ico / .nm-kicker / .nm-read
                                       — the dialog centres instead of ruling
     V1.7  .ev-badges                  — the badges merge into `.ev-tags`
     V1.9  .dash-body / .dash-grip / --dash-ratio
                                       — the top row is a fixed 4:1, nothing drags
     V1.9  .dist / .dist-row / .dist-label / .dist-track / .dist-fill
                                       — Topic Distribution became Following
     V1.9  .nc-pri                     — the note card's priority is its surface
     V1.9  .ev-cats                    — the category row moved into the head
     V1.16 .db-count                   — the table's own `#` column says it
   ========================================================================== */

/* ---------- tokens ---------- */
:root {
  --bg:            #FBFAF8;
  --surface:       #FFFFFF;
  --surface-2:     #F5F4F1;
  --surface-3:     #EFEDE8;
  --border:        #E6E3DD;
  --border-strong: #D3CFC6;

  --ink:      #14120F;
  --ink-2:    #45423C;
  --ink-3:    #6B6862;
  --muted:    #8C8880;

  --accent:       #C8410F;
  --accent-hover: #A83509;
  --accent-soft:  #FBEDE5;
  /* The tinted hairline used by the why-box border and the note margin rule. */
  --accent-line:  #F2DACB;

  /* §0 — the Watch priority. Low-saturation blush instead of grey, so "观察池"
     reads as its own soft signal next to the orange S everywhere it appears:
     gantt bars, label ticks, the density strip, the badge and the toggle. */
  --watch:       #E7C3D0;
  --watch-strong: #D6A4B8;
  --watch-soft:  #F6E5EC;
  --watch-ink:   #93566D;
  --watch-line:  #E5B9C8;

  /* §9 — the "following" pennant. A red that is a shade warmer than the orange
     S accent, so a raised flag reads as its own mark and never as a priority. */
  --flag:        #C8392B;
  --flag-soft:   #FBEDEA;
  --flag-line:   #EDCEC8;

  /* V1.12 — "the priority band". In the two scanning surfaces (the Dashboard's
     event list and its Following list) the priority is no longer a badge: the
     title itself sits on a band. The band uses the SAME two colours as every
     other priority mark in the app — the timeline bars, the Database priority
     badge and the Radar gantt bars — so nothing invents a third tint:
       S-level  -> `--accent` with white text (the badge's solid fill / the
                   gantt bar's fill)
       Watch    -> `--watch` with `--watch-ink` text (the blush bar, the badge)
     There is deliberately no `--band-*` token: a check of the two would be a
     check of a copy. */

  /* §6 — "warm note paper": the chosen surface for anything whose content is a
     note. One tone for the card, a hairline of its own so it does not borrow the
     cold grey border, a deeper patch for the why-block, and a cooled-down margin
     rule. Keeping all four here is what lets the grid card and the dialog read as
     the same material. */
  --paper:       #FBF6ED;
  --paper-2:     #F7E9D8;
  --paper-line:  #EFE7D9;
  --paper-line-2:#DCCDB4;
  --paper-rule:  #E3D3BB;

  --ok:        #1A7F53;
  --ok-soft:   #E6F4EC;
  --warn:      #96601A;
  --warn-soft: #FBF0DC;
  --info:      #2A4FBF;
  --info-soft: #E9EEFD;
  --violet:    #6A36C4;
  --violet-soft:#F1EAFD;
  --danger:    #A32A22;

  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
               'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  --font-serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia,
                'Songti SC', 'Songti TC', serif;
  --font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas,
               'Liberation Mono', monospace;

  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;

  --shadow-1: 0 1px 2px rgba(20, 18, 15, .04);
  --shadow-2: 0 4px 16px -6px rgba(20, 18, 15, .12);
  --shadow-3: 0 24px 60px -20px rgba(20, 18, 15, .28);

  --nav-h: 56px;

  /* §2 — content stops growing past this width and centres instead, so nothing
     needs 90% browser zoom on a wide monitor. */
  --content-max: 1340px;

  /* §1 — the density track keeps a fixed height so the two panels below it can
     be sized by what is left of the viewport rather than by their contents. */
  --tl-h: 104px;

  /* §3 — the Notes area of the event card. This is the number that decides the
     card's height: the card is exactly head + why (2 rows) + this. */
  --ev-notes-h: 150px;

  /* gantt */
  --g-label: 224px;
  --band-weekend: rgba(20, 18, 15, .035);
  --band-today: rgba(200, 65, 15, .10);

  /* one shared chevron so every <select> in the app looks identical */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6' viewBox='0 0 9 6'%3E%3Cpath d='M1 1l3.5 3.5L8 1' fill='none' stroke='%238C8880' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ---------- reset ---------- */
* { box-sizing: border-box; }
/* `[hidden]` is a user-agent rule, so ANY explicit `display` beats it — which is
   how a `.btn` (an `inline-flex`) once sat on screen while the DOM insisted it
   was hidden. Putting the attribute back in charge costs one rule and removes a
   whole class of "why is that still there". */
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; }
::selection { background: var(--accent-soft); }

/* ---------- small utilities ---------- */
.micro {
  font-size: 10px; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}
.num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.serif { font-family: var(--font-serif); }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============================================================================
   shell (§5)
   ========================================================================== */
.app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }

/* The bar is a 3-track grid so the navigation is centred against the *window*,
   not against whatever the brand / tools happen to measure. */
.topbar {
  flex: 0 0 auto; height: var(--nav-h); z-index: 40;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 16px; padding: 0 20px;
  background: rgba(251, 250, 248, .94);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
.brand-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
  flex: 0 0 8px; box-shadow: 0 0 0 3px var(--accent-soft);
}
.brand-txt { display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.brand-txt b {
  font-family: var(--font-serif); font-size: 16px; font-weight: 600;
  letter-spacing: -.01em; white-space: nowrap;
}
.brand-txt i {
  font-style: normal; font-size: 11px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.nav { display: flex; align-items: center; gap: 4px; justify-self: center; }
.nav-item {
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 14px; border-radius: 99px;
  font-size: 13px; font-weight: 500; color: var(--ink-3);
  transition: background .12s, color .12s;
}
.nav-item .ico { flex: 0 0 auto; opacity: .75; }
.nav-item:hover { background: var(--surface-2); color: var(--ink); }
.nav-item.is-active { background: var(--ink); color: #fff; font-weight: 600; }
.nav-item.is-active .ico { opacity: 1; }

.top-actions {
  display: flex; align-items: center; gap: 10px; justify-self: end; min-width: 0;
}
.saved { font-size: 11px; color: var(--muted); white-space: nowrap; }
/* V1.17 — the published-dataset readout. It stays on screen on a phone, where
   `.saved` is hidden, because "is this device on the release I just pushed?" is
   the one question a second device has to be able to answer at a glance. */
.data-ver {
  flex: 0 0 auto;
  font-size: 10.5px; font-weight: 650; line-height: 1;
  color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap;
  padding: 4px 7px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-2);
}
/* It is a readout, not a control -- but a stale one should be noticeable, so a
   device that has not taken the published release greys it further down. */
.data-ver.is-stale { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }

.main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 6px; justify-content: center;
  height: 34px; padding: 0 13px; border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface);
  font-size: 12.5px; font-weight: 550; color: var(--ink-2);
  white-space: nowrap; transition: background .12s, border-color .12s, color .12s;
}
.btn:hover { background: var(--surface-2); color: var(--ink); border-color: var(--border-strong); }
.btn:active { transform: translateY(.5px); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
.btn.is-danger { color: var(--danger); }
.btn[disabled] { opacity: .45; pointer-events: none; }

/* ---------- badge ---------- */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  height: 20px; padding: 0 7px; border-radius: 4px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .02em;
  white-space: nowrap; border: 1px solid transparent;
}
.badge-s { background: var(--accent); color: #fff; }
.badge-watch { background: var(--watch-soft); color: var(--watch-ink); border-color: var(--watch-line); }
.badge-cat { background: var(--surface-2); color: var(--ink-3); border-color: var(--border); font-weight: 500; }
.badge-outline { border-color: var(--border-strong); color: var(--muted); }
.badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-violet { background: var(--violet-soft); color: var(--violet); }

/* ============================================================================
   layout blocks
   ========================================================================== */
.view {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  padding: 0 24px 56px;
}
/* The top padding lives on the inner column, not on the scroll container:
   a `position: sticky` child is pinned inside the scroller's *content* box, so
   padding on `.view` would leave a 22px band of scrolled-away content showing
   above the sticky table header. */
.view-inner { width: 100%; max-width: var(--content-max); margin: 0 auto; padding-top: 22px; }
/* §1 / §4 — the Dashboard masthead and the Notes header both sat 22px below the
   nav, which is a band of nothing on the two pages that begin with a header row
   of their own. Trimmed, not removed on Notes: a locked page needs the header to
   read as a header, not as part of the bar above it. */
/* §1 — per-page trims of the 22px band. The class suffix is the route name. */
.view.is-dashboard .view-inner { padding-top: 0; }
/* §2 — the Database is the one page whose content is a nine-column table, so it
   is the one page allowed more than `--content-max`. The why column needs 340px
   before anything else is allocated; on a 1440-wide window the default cap left
   the table 1338px, i.e. exactly the width the binding row needed and no margin
   at all. */
.view.is-database .view-inner { max-width: 1560px; padding-top: 0; }
.view.is-notes .view-inner { padding-top: 10px; }

/* Locked pages (§1): the page itself does not scroll, only an inner list. */
.view.is-lock { overflow: hidden; padding-bottom: 18px; }
.view.is-lock .view-inner { height: 100%; display: flex; flex-direction: column; min-height: 0; }

.section-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 11px; }
.section-head h2 { font-size: 14px; font-weight: 650; letter-spacing: -.005em; }
.section-head .hint { font-size: 11.5px; color: var(--muted); }
.section-head .head-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }

/* ---------- dashboard (§1) ---------- */
/* Two bands (V1.10). The TOP band is a fixed height and pinned to the top of the
   locked page: the density timeline on the left (four fifths) and the Topic
   Distribution bars on the right (one fifth), stretched to the same height.
   Its height is 65% of the 312px it measured with the original, roomier bars.
   The BOTTOM band yields: the events list on the left and the Following list on
   the right, divided by a draggable seam that opens at 3:1. */
.dash { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.dash-top {
  flex: 0 0 auto; height: var(--dash-top-h, 203px); margin-bottom: 16px;
  display: grid; gap: 14px;
  grid-template-columns: minmax(0, 4fr) minmax(0, 1fr);
  align-items: stretch;
}
.dash-tl, .dash-topic { min-width: 0; min-height: 0; display: flex; }
/* The timeline card stretches to the band's height and its track grows with it,
   so the density columns still run down to the card's bottom edge instead of
   leaving the card half empty. */
.dash-tl .tl-card { width: 100%; display: flex; flex-direction: column; }
.dash-tl .tl-track { flex: 1 1 auto; min-height: var(--tl-h); }
.topic-panel .panel-fill { padding: 8px 12px; overflow-y: auto; }

/* The bottom band: events | seam | Following. The seam's `--bottom-ratio` is a
   `fr` value written by the drag handler, so a gesture writes one custom
   property and nothing re-renders. */
.dash-bottom {
  flex: 1 1 auto; min-height: 0;
  display: grid; gap: 0;
  grid-template-columns: minmax(0, var(--bottom-ratio, 3fr)) 18px minmax(0, 1fr);
}
.dash-events, .dash-follow { min-width: 0; min-height: 0; display: flex; }
.dash-grip {
  position: relative; align-self: stretch; cursor: col-resize;
  display: flex; align-items: center; justify-content: center;
  touch-action: none;
}
/* The seam is a hairline at rest and a 2px accent while you are using it, so it
   reads as a divider until it is asked to behave as a control. */
.dash-grip::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px;
  transform: translateX(-50%); background: var(--border);
  transition: background .12s, width .12s;
}
.dash-grip i {
  position: relative; width: 4px; height: 34px; border-radius: 99px;
  background: var(--border-strong); opacity: 0; transition: opacity .12s;
}
.dash-grip:hover i, .dash-grip:focus-visible i, .dash-grip.is-dragging i { opacity: 1; }
.dash-grip:hover::before, .dash-grip.is-dragging::before {
  background: var(--accent); width: 2px;
}
.dash-grip:focus-visible { outline: none; }
body.is-resizing { cursor: col-resize; user-select: none; }
body.is-resizing * { cursor: col-resize !important; }

/* The Following panel's cards: the same facts as a list row, stacked for the
   narrow column, and each one opens that event's note. */
/* V1.14 — the cards are content-sized again: the three-row grid that showed
   exactly three of them clipped the longer descriptions, and the request was
   the other way round — show each card whole and let the column scroll. */
.follow-list { display: flex; flex-direction: column; gap: 10px; }
.follow-card {
  display: flex; flex-direction: column; gap: 4px;
  width: 100%; text-align: left; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  transition: background .12s, border-color .12s;
}
.follow-card:hover { background: var(--surface-2); border-color: var(--border-strong); }
.fc-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.fc-date { font-size: 11px; color: var(--ink-2); flex: 0 0 auto; }
/* §3 — the location is pulled to the right-hand edge of the first line. */
.fc-loc {
  margin-left: auto; font-size: 11px; color: var(--muted); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fc-name {
  font-size: 12.5px; font-weight: 600; line-height: 1.35;
  align-self: flex-start; max-width: 100%;
}
.fc-name.band-s, .fc-name.band-watch { padding: 2px 8px; border-radius: 6px; }
.fc-name.band-s { background: var(--accent); color: #fff; }
.fc-name.band-watch { background: var(--watch); color: var(--watch-ink); }
/* §3 — both prose fields live on the card and are ordered by weight: the
   objective description is a quiet grey note, while the why-line is the reason
   the panel exists, so it takes the body ink and a margin rule of its own.
   V1.14 — neither is clamped any more: the card is as tall as its content, and
   an empty field renders nothing at all rather than reserving a blank line. */
.fc-desc {
  font-size: 10.5px; color: var(--muted); line-height: 1.45;
}
.fc-why {
  margin-top: 2px; padding-left: 8px;
  border-left: 2px solid var(--accent-line);
  font-size: 11.5px; color: var(--ink-2); line-height: 1.5;
}
.follow-card.is-watch .fc-why { border-left-color: var(--watch-line); }

/* ---------- generic inner panel (used by the dashboard columns) ---------- */
.panel {
  flex: 1 1 auto; min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1); overflow: hidden;
}
.panel-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
  padding: 11px 15px; border-bottom: 1px solid var(--border);
}
.panel-head h2 { font-size: 13px; font-weight: 650; }
.panel-head .hint { font-size: 11.5px; color: var(--muted); }
.panel-head .head-right { margin-left: auto; }
.panel-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.panel-fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 15px; display: flex; }
.panel-empty { padding: 28px 16px; text-align: center; color: var(--muted); font-size: 12.5px; }
/* The Following column scrolls inside the bottom band like the events list.
   The cards carry their own border, so the scroller keeps only an inset. */
.follow-panel .panel-scroll { padding: 10px 11px; }

/* ---------- radar list ---------- */
/* §2 (V1.14) — six events fill the column: the row height is a sixth of the
   scrollport instead of whatever each row's content happens to need, which is
   what turns "as many as fit" into "exactly six" at the default window. A
   seventh is what the column scrolls for. */
.radar-list {
  display: grid; gap: 0; height: 100%;
  grid-auto-rows: calc(100% / 6);
}
.radar-row {
  display: grid; grid-template-columns: 88px 1fr auto;
  gap: 14px; padding: 10px 15px; text-align: left; width: 100%;
  border-top: 1px solid var(--border); align-items: start;
  min-height: 0; overflow: hidden;
  transition: background .12s;
}
.radar-row:first-child { border-top: 0; }
.radar-row:hover { background: var(--surface-2); }
.radar-date {
  font-family: var(--font-mono); font-size: 12px; color: var(--ink-2);
  padding-top: 2px; line-height: 1.5;
}
.radar-date b { font-weight: 600; }
.radar-main { min-width: 0; }
/* §8 — name and location on one line: the name truncates, the location keeps
   its natural width on the right.
   V1.12 — the priority badge is gone. `.radar-name` carries the priority as a
   band instead, so the title is now the marker: it is `flex: 0 1 auto` (it hugs
   its words rather than filling the column, which is what makes the wash read
   as a highlight) and the location is pushed to the right edge by an auto
   margin. The band hugs by shrinking — never by wrapping — so the row stays
   one line tall no matter how long the name is. */
.radar-name-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.radar-name {
  flex: 0 1 auto; min-width: 0;
  font-size: 13.5px; font-weight: 600; letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.radar-name.band-s, .radar-name.band-watch { padding: 3px 9px; border-radius: 6px; }
.radar-name.band-s { background: var(--accent); color: #fff; }
.radar-name.band-watch { background: var(--watch); color: var(--watch-ink); }
.radar-loc { font-size: 11.5px; color: var(--muted); flex: 0 0 auto; white-space: nowrap; margin-left: auto; }
.radar-why { display: block; margin-top: 6px; font-size: 12.5px; color: var(--ink-2); }
.radar-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }

/* ---------- topic distribution bars (§1) ---------- */
/* One row per umbrella: name · count, then a proportional bar. Compact enough
   that all five umbrella rows fit the fixed top band without scrolling. */
.dist { flex: 1 1 auto; display: flex; flex-direction: column; gap: 4px; justify-content: space-between; }
.dist-row { display: grid; grid-template-columns: 1fr auto; gap: 3px; align-items: center; width: 100%; padding: 1px 6px; border-radius: var(--r-sm); }
.dist-row:hover { background: var(--surface-2); }
.dist-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 11.5px; line-height: 1.2; color: var(--ink-2); }
.dist-label span:last-child { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px; }
.dist-track {
  grid-column: 1 / -1; height: 5px; border-radius: 99px;
  background: var(--surface-3); overflow: hidden;
}
.dist-fill { display: block; height: 100%; border-radius: 99px; background: var(--accent); }

/* ============================================================================
   Event density timeline (§1, §2)
   The track is a plain flex row of equal columns and it is always exactly as
   wide as its container, so nothing pans sideways. The window pager sits in the
   header, which is what re-scopes every dashboard part at once — the day
   columns, the Following panel and the event list are three views of one slice.
   ========================================================================== */
.tl-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1);
  padding: 11px 16px 13px;
}
/* V1.12 — one row, three seats: the count alone on the LEFT, the pager
   (`← range →`) dead centre, the window selector on the right. A grid rather
   than a flex row, because only `1fr auto 1fr` keeps the middle seat on the
   card's centre line when the two outer seats are different widths. */
.tl-head {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 10px 14px; margin-bottom: 11px;
}
.tl-count {
  justify-self: start; font-size: 11.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.tl-zoom { justify-self: end; }

/* §1 — ← range → : the label doubles as "jump back to today". */
.tl-nav { display: inline-flex; align-items: center; gap: 2px; justify-self: center; }
.tl-arrow {
  width: 28px; height: 28px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm); font-size: 14px; line-height: 1; color: var(--ink-3);
  transition: background .12s, color .12s;
}
.tl-arrow:hover { background: var(--surface-2); color: var(--ink); }
.tl-range {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  padding: 2px 11px; border-radius: var(--r-sm); text-align: left;
  transition: background .12s;
}
.tl-range:hover { background: var(--surface-2); }
.tl-range b {
  font-size: 13.5px; font-weight: 650; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.tl-range i { font-style: normal; font-size: 11px; color: var(--muted); white-space: nowrap; }

.tl-track { display: flex; align-items: stretch; height: var(--tl-h); gap: 0; }
.tl-col {
  flex: 1 1 0; min-width: 0; position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 1px 16px; border-radius: 4px;
  transition: background .1s;
}
.tl-col:hover { background: var(--surface-2); }
/* §1 (V1.12) — the stack's height is written inline (a % of the track's content
   box, scaled against the busiest day in the window) and the bars share it
   equally, so a busy day is a tall column of bars and a quiet day is a short
   one. `min-height: 0` + `overflow: hidden` keep a very short column from
   bleeding up past the head when it has more bars than pixels. */
.tl-bars {
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center; gap: 2px; min-height: 0; overflow: hidden;
}
.tl-bar {
  display: block; width: 100%; max-width: 16px; min-height: 0;
  border-radius: 2px; background: var(--border-strong); flex: 1 1 0;
}
.tl-bar.s { background: var(--accent); }
.tl-bar.w { background: var(--watch); }
.tl-tick {
  position: absolute; bottom: 1px; left: 0; right: 0;
  font-size: 9.5px; color: var(--muted); font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center;
}
.tl-dow { font-style: normal; margin-right: 3px; }
.tl-col.is-today .tl-tick { color: var(--accent); font-weight: 700; }
.tl-col.is-today::after {
  content: ''; position: absolute; left: 50%; top: 0; bottom: 16px;
  width: 1px; margin-left: -.5px; background: var(--band-today);
}

/* ============================================================================
   filters + table (§3)
   ========================================================================== */
/* §2 (V1.10) — the Database header is three EQUAL-HEIGHT modules in one sticky
   row: the selectors, the count, and the actions. Each is its own bordered box,
   stretched to the tallest, so the row reads as three blocks rather than one
   long bar. It sits flush under the nav: `.view-inner` loses its top padding on
   this page (see below), so nothing floats above it. */
.db-bar {
  position: sticky; top: 0; z-index: 6;
  display: grid; gap: 12px;
  /* V1.16 — the selectors box is deliberately narrower than the space the row
     would give it: 3.4fr : 1fr lands it on 87% of the width it had when it was
     the only thing in the row (1055px of 1377 at the app's widest window) and
     hands the difference to the action box, which now carries four buttons. The
     split is a ratio, so it holds at every window instead of only at one. */
  grid-template-columns: minmax(0, 3.4fr) minmax(0, 1fr);
  align-items: stretch;
  margin-bottom: 13px;
}
.db-mod {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1);
  padding: 10px 14px;
  display: flex; flex-direction: column; justify-content: center;
}
.db-mod-actions { min-width: 0; }
/* V1.16 — four buttons in a 2×2 grid rather than a stack: a fourth row would
   have made the sticky bar taller than the table header it has to sit above,
   and a taller sticky bar costs the table a row of height for nothing. */
.db-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.db-actions .btn { width: 100%; justify-content: center; }

/* V1.16 — the selectors spread across their box instead of stacking up against
   its left edge. `space-between` only ever puts space BETWEEN the groups, never
   outside the first or the last, which is what keeps the two ends tight; the
   drawer (a narrow column of the same markup) keeps the old flex-start, where a
   wrapped line has to read from the left. */
.db-mod-filters .filter-row { justify-content: space-between; gap: 12px 14px; }

/* V1.16 — the "24/50" statistic and its `.db-count` rule are gone; the table's
   own # column answers the same question one row at a time. */

.filter-row { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: flex-start; }
.filter-group { display: flex; flex-direction: column; gap: 6px; }
.filter-inline { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* Every control under a filter label is the same height, so the four labels sit
   on one line instead of stepping up and down with whatever is under them. */
.filter-group > .segmented,
.filter-group > .chip,
.filter-group > .chips,
.filter-group > .filter-inline { min-height: 30px; }
.filter-group > .chips { align-items: center; }
.filter-group .segmented button { padding: 0 12px; height: 28px; }
.filter-group .chip { height: 28px; }
.filter-group .select, .filter-group .input { height: 30px; }
.filter-label { font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.arrow-sep { color: var(--muted); }

.segmented {
  display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm);
  overflow: hidden; background: var(--surface);
}
.segmented button {
  padding: 5px 11px; font-size: 12px; color: var(--ink-3);
  border-right: 1px solid var(--border);
  transition: background .12s, color .12s;
}
.segmented button:last-child { border-right: 0; }
.segmented button:hover { background: var(--surface-2); }
.segmented button.is-on { background: var(--ink); color: #fff; font-weight: 600; }

.search {
  display: flex; align-items: center; gap: 7px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 0 10px; height: 32px; width: 100%;
}
.search:focus-within { border-color: var(--border-strong); box-shadow: 0 0 0 3px rgba(20,18,15,.04); }
.search input { border: 0; outline: 0; background: none; width: 100%; font-size: 13px; }
.search .s-ico { color: var(--muted); flex: 0 0 auto; display: block; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  height: 25px; padding: 0 9px; border-radius: 99px;
  border: 1px solid var(--border); background: var(--surface);
  font-size: 11.5px; color: var(--ink-3); display: inline-flex; align-items: center; gap: 5px;
}
.chip:hover { border-color: var(--border-strong); color: var(--ink); }
.chip.is-on { background: var(--ink); border-color: var(--ink); color: #fff; font-weight: 550; }
.chip .x { opacity: .6; font-size: 12px; }

.select, .input {
  height: 32px; border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); padding: 0 9px; font-size: 12.5px; outline: 0;
}
.select:focus, .input:focus { border-color: var(--border-strong); }
.input-date { font-family: var(--font-mono); font-size: 12px; }
/* A native select brings the OS arrow, which is the one control in the app that
   refuses to match the hairline language — so it is replaced, once, here. */
select.select, select.ef {
  appearance: none; -webkit-appearance: none;
  padding-right: 26px;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 9px center;
}

/* The table is `table-layout: fixed` with percentage columns, so it always fits
   the container — no horizontal scrolling at any width. `border-collapse:
   separate` is required for the sticky header to keep its bottom border. */
.table-wrap {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
table.data {
  width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0;
}
table.data thead th {
  /* §5 — sticks *below* the sticky filter bar, never underneath it. The bar's
     height is published as `--filters-h` by syncFilterHeight() because it
     rewraps with the viewport. */
  position: sticky; top: var(--filters-h, 0px); z-index: 3;
  background: var(--surface-2); padding: 9px 11px; text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: 10px; font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
  border-bottom: 1px solid var(--border);
}
table.data thead th:first-child { border-top-left-radius: var(--r-lg); }
table.data thead th:last-child { border-top-right-radius: var(--r-lg); }
table.data thead th.sortable { cursor: pointer; user-select: none; }
table.data thead th.sortable:hover { color: var(--ink); }
table.data thead th .arrow { opacity: .55; margin-left: 3px; }
table.data tbody tr { cursor: pointer; }
table.data tbody tr:hover { background: #FCFBF9; }
/* §4 (V1.12) — an event that is over is greyed, so the "still ahead of me"
   block ends visibly rather than only by date arithmetic. */
table.data tbody tr.is-ended { background: var(--surface-2); }
table.data tbody tr.is-ended:hover { background: var(--surface-3); }
table.data td {
  padding: 10px 11px; vertical-align: middle; font-size: 12.5px; text-align: center;
  border-bottom: 1px solid var(--border);
  overflow: hidden; text-overflow: ellipsis;
}
table.data tbody tr:last-child td { border-bottom: 0; }
table.data td.nowrap { white-space: nowrap; }

/* Column plan (V1.16): flag · # · name · why · date · location · priority ·
   category · organizer · website · delete
   §2 — Event Name and Why share the same share (21% each) and Why sits directly
   to the right of the name, so the objective subtitle and the subjective
   why-line are read together. The flag column is a fixed 44px (one 26px button
   plus the cell padding), the row number a fixed 30px, the website column just
   fits the word above it, and the delete column a fixed 49px.
   V1.12 — the organizer column is the ONLY column without a width, so in the
   fixed table algorithm it is the one that absorbs every pixel the others leave
   over. That is what "give the rest to the organizer" means mechanically: the
   other proportions stay exactly as they are and this column tracks the
   viewport.
   §2 — the header row is centred and so is every data cell EXCEPT the two prose
   columns, which stay left-aligned so a wrapped line keeps a straight edge. */
table.data th:nth-child(1) { width: 44px; }
table.data th:nth-child(2) { width: 30px; }
table.data th:nth-child(3) { width: 21%; }
table.data th:nth-child(4) { width: 21%; }
table.data th:nth-child(5) { width: 9%; }
table.data th:nth-child(6) { width: 9%; }
table.data th:nth-child(7) { width: 7%; }
table.data th:nth-child(8) { width: 9%; }
/* 9 (organizer) — deliberately no width: it takes what is left. */
table.data th:nth-child(10) { width: 74px; }
table.data th:nth-child(11) { width: 49px; }

/* The two prose columns keep a left edge in the DATA rows (name 3, why 4) and
   stay top-aligned — a wrapped paragraph looks wrong vertically centred against
   a one-line neighbour. Every other cell is centred both ways (V1.12), which is
   what makes a row of short facts read as a row rather than as a ragged edge. */
table.data tbody td:nth-child(3), table.data tbody td:nth-child(4) {
  text-align: left; vertical-align: top;
}

/* V1.12 — the flag cell holds one 26px control in a 44px column, so the cell's
   own padding was pushing it past the content box and the inherited
   `text-overflow: ellipsis` was painting a stray ".." beside an icon that was
   itself fully visible. No padding, no clipping — it simply fits. */
table.data td.cell-flag {
  padding-left: 0; padding-right: 0;
  overflow: visible; text-overflow: clip;
}

.cell-name { font-weight: 600; font-size: 13px; line-height: 1.4; white-space: normal; }
/* V1.16 — the name is READ as text and renamed on click, rather than being a
   permanently open field: a single-line input in a 21% column cuts "…Shows —
   Phase 1" down to "…Shows — Phase", and a name is the one thing in this table
   that must never be shown half. The dashed rule that appears under the pointer
   is the only affordance it needs; the field itself is the app's normal `.ef`. */
.cell-name .nm-text {
  display: block; cursor: text;
  border-bottom: 1px dashed transparent;
}
.cell-name .nm-text:hover { border-bottom-color: var(--border-strong); }
.cell-name .nm-text:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
.cell-name .ef-name {
  display: block; width: 100%; height: auto; min-height: 20px;
  padding: 0 4px; margin-left: -4px;
  font-size: 13px; font-weight: 600; line-height: 1.4;
}
.cell-name .sub {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-weight: 400; font-size: 11.5px; color: var(--muted); margin-top: 2px;
}
table.data td .sub { display: block; font-size: 11px; color: var(--muted); }
.loc-city { display: block; }
.cell-cats { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
/* §9 — three lines of organizer: two was cutting names that run to a long
   bilingual string ("香港贸易发展局 HKTDC × Messe Frankfurt (HK)"). */
.cell-org {
  color: var(--ink-3); font-size: 12px;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
/* §2 (V1.8) — the why-line in the table. It used to clamp at two lines, but now
   that the column is as wide as Event Name, the line has room to breathe and is
   allowed to run past two lines before it is clipped, so the longest reads stay
   whole. */
.cell-why {
  color: var(--ink-3); font-size: 11.5px; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
}
/* V1.12 — the website cell is one glyph. The column is only as wide as the word
   "Website" above it, and every pixel that frees goes to the organizer. */
.link-out {
  display: inline-flex; align-items: center; justify-content: center; gap: 3px;
  color: var(--info); text-decoration: none; line-height: 1;
}
.link-out .ico-link { display: block; }
.link-out:hover { color: var(--accent); }
.link-n { font-size: 10px; font-weight: 650; font-variant-numeric: tabular-nums; }
.nolink { font-size: 11.5px; color: var(--warn); }

/* §2 — delete, from the list. It is kept faint so a table of 50 rows does not
   turn into a field of red buttons, and it comes up to full contrast when you
   are actually on the row you mean. It always confirms. */
.cell-act { text-align: center; }
.row-del {
  width: 26px; height: 26px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--muted); opacity: .3;
  transition: opacity .12s, background .12s, color .12s;
}
table.data tbody tr:hover .row-del { opacity: .75; }
.row-del:hover { opacity: 1; background: var(--surface-3); color: var(--danger); }
.row-del:focus-visible { opacity: 1; outline: none; box-shadow: 0 0 0 3px rgba(163,42,34,.14); }

/* mobile cards */
.card-list { display: none; flex-direction: column; gap: 10px; }
.ev-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 13px; text-align: left; width: 100%;
  box-shadow: var(--shadow-1);
}
.ev-card-top { display: flex; align-items: flex-start; gap: 10px; }
.ev-card-main { flex: 1 1 auto; min-width: 0; }
.ev-card-date { display: block; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); margin-top: 3px; }
.ev-card-date:first-child { margin-top: 0; }
.ev-card-name { display: block; font-size: 13.5px; font-weight: 650; margin-top: 3px; line-height: 1.4; }
.ev-card-meta { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }

.empty {
  padding: 52px 20px; text-align: center; color: var(--muted);
  border: 1px dashed var(--border-strong); border-radius: var(--r-lg);
}
.empty b { display: block; color: var(--ink-2); font-size: 14px; margin-bottom: 5px; }

/* ============================================================================
   Notes page (§3)
   Derived entirely from `events.filter(hasNote)`, so there is no second store to
   keep in sync: editing a note on a card writes straight to its event.
   The order is fixed (newest event first) and the only narrowing control is a
   custom date range.
   ========================================================================== */
.notes-head { flex-wrap: wrap; }
.notes-head .hint { flex: 0 1 auto; }
.notes-search { width: 216px; flex: 0 0 auto; }

.range { display: flex; align-items: center; gap: 7px; }
.range-label {
  font-size: 10px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}

/* §8 — two full rows of three are visible at 100% zoom: the shell row is
   `(available - 1 gap) / 2`, so row 2 ends exactly at the fold and the grid —
   not the page — scrolls to reveal any more. The card no longer needs a fixed
   height of its own; the row dictates it. */
.notes-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: calc((100% - 16px) / 2);
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
}
/* §6 / §8 — the card is warm paper: cream surface, its own warm hairline, and a
   soft drop shadow that lifts it off the page like a physical note. A faint
   bottom-edge crease plus the shadow reads as "a sheet of paper", without any
   texture that would fight the type. */
.note-card {
  display: flex; flex-direction: column;
  height: 100%; min-height: 0; min-width: 0; overflow: hidden;
  background: var(--paper); border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  padding: 17px 19px 16px;
  text-align: left; cursor: pointer;
  transition: border-color .12s, box-shadow .12s, transform .12s;
  box-shadow: 0 1px 0 var(--paper-line), 0 3px 10px -4px rgba(90, 70, 40, .18);
}
.note-card:hover {
  border-color: var(--paper-line-2);
  box-shadow: 0 1px 0 var(--paper-line-2), 0 8px 18px -6px rgba(90, 70, 40, .24);
  transform: translateY(-1px);
}
/* §9 — the surface IS the priority now: paper for an S-level event (unchanged),
   plain white for one on the watch list. The S / Watch badge is gone. */
.note-card.is-watch {
  background: var(--surface); border-color: var(--border);
  box-shadow: var(--shadow-1);
}
.note-card.is-watch:hover {
  border-color: var(--border-strong); box-shadow: var(--shadow-2);
}
.note-card.is-watch .nc-notes { border-left-color: var(--border); }
.note-card.is-watch .why-box.is-flat { background: var(--surface-2); }
.note-card:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* §9 — the top block is sized by its own content. It used to be capped at 50%
   of the card and clipped, which on a shorter window cut the title in half and
   folded the why-line away entirely. The note below takes the remainder. */
.nc-top {
  flex: 0 0 auto; min-height: 0;
  display: flex; flex-direction: column; gap: 8px;
}
/* Row 1: date · city, country · ... · category tags (the seat the S badge had). */
.nc-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
.nc-date { flex: 0 0 auto; font-size: 12px; color: var(--ink-2); letter-spacing: -.01em; }
.nc-loc {
  font-size: 11.5px; color: var(--muted); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nc-cats { margin-left: auto; flex: 0 0 auto; display: inline-flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.nc-name {
  font-size: 15px; font-weight: 650; letter-spacing: -.015em; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* The note hangs off a margin rule, the way a note hangs off a line in a
   notebook. That rule is the whole visual idea: it says "the writing is this
   part" without a heading or a box. On paper it is warmed down to `--paper-rule`
   so the card holds a single accent mark (the bulb) rather than two. */
.nc-notes {
  flex: 1 1 auto; min-height: 56px;
  margin-top: 12px; padding: 2px 2px 0 13px;
  border-left: 2px solid var(--paper-rule);
  overflow-y: auto;
}

/* §4 — the note dialog. Its furniture is gone: no "Note" kicker, no pencil
   badge, no margin rule, and no hairline between the head and the why-box. The
   head and the why-box are centred instead; the note body stays full width,
   because a document that is centred is a poster, not a page.
   Every rule here is written as `.modal.note-modal` / `.modal-head.nm-head` on
   purpose: this block sits EARLIER in the file than the generic `.modal` rules,
   and a bare `.note-modal` loses the tie on source order. */
.modal.note-modal {
  width: min(720px, 100%);
}
.modal-head.nm-head {
  align-items: flex-start; gap: 12px;
  padding: 18px 20px 6px; border-bottom: 0;
}
/* §4 — `text-align: center` centres inside a BOX, and a `flex: 1` head sits in
   the row beside the close button, so its box centre is (button + gap) / 2 — i.e.
   18px — left of the dialog's own centre line. A spacer of exactly the button's
   width on the other side puts the middle column back on the true centre. One
   variable holds the button footprint, so the two can never drift apart.
   Both centred heads (the note dialog and the event card) share it — they are
   the same problem, so they get the same answer. */
.modal-head.nm-head, .modal-head.ev-head { --mh-x-w: 24px; }
.modal-head.nm-head::before, .modal-head.ev-head::before { content: ''; flex: 0 0 var(--mh-x-w); }
.modal-head.nm-head .x, .modal-head.ev-head .x { flex: 0 0 var(--mh-x-w); }
.nm-head-main {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
.nm-title { font-size: 17px; font-weight: 650; letter-spacing: -.015em; line-height: 1.28; }
.nm-meta {
  display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap;
  font-size: 11.5px; color: var(--muted); margin-top: 7px;
}
.nm-cats { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.modal-body.nm-body { padding: 8px 20px 16px; display: flex; flex-direction: column; gap: 14px; }
/* §4 (V1.10) — the why-line hugs its own text and centres, instead of stretching
   to the full width of the note below it and leaving a wide blank either side.
   `align-self: center` is what centres it; `max-width` keeps a long sentence
   from running past the dialog. */
.nm-body .why-box { align-self: center; width: fit-content; max-width: 100%; }
/* §4 — in the note dialog the why-line reads as a centred pull-quote above the
   note body, not a left-ragged field. The icon stays put so the box still reads
   as "the why", but the sentence itself centres. */
.nm-body .why-box .why-txt p { text-align: center; }

/* §4 (V1.10) — a Watch-level note opens on a NEUTRAL surface: white reading pane
   with a grey hairline, grey why-patch. That is the same distinction the note
   cards make on the page (paper for S-level, white for the watch list), carried
   into the dialog so opening a card does not change what the note is made of. */
.modal.note-modal.is-watch .md.md-preview { background: var(--surface); border-color: var(--border); }
.modal.note-modal.is-watch .why-box.is-flat { background: var(--surface-2); }
.modal.note-modal.is-watch .nm-note .md-input { background: var(--surface); }
.nm-note { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.nm-note-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.nm-note .md-toggle { margin-left: auto; }
.nm-note .md-input, .nm-note .md-preview { flex: 1 1 auto; min-height: 216px; overflow-y: auto; }
/* §2 (V1.14) — the note footer is three seats: what was saved on the left, the
   record's own links dead centre, the actions on the right. A grid rather than
   the shared flex row, because two competing `margin: auto` (which is what the
   flex row would need) centres the middle seat between whatever the outer two
   happen to measure — not on the card. */
.modal-foot.nm-foot {
  display: grid; grid-template-columns: 1fr auto 1fr;
  padding-left: 20px;
}
.modal-foot.nm-foot .left { margin-right: 0; justify-self: start; min-width: 0; }
.nm-acts { justify-self: end; display: flex; gap: 10px; align-items: center; }
.nm-links { justify-self: center; display: inline-flex; align-items: center; gap: 6px; }
.nm-link {
  width: 28px; height: 28px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .12s, color .12s;
}
.nm-link:hover { background: var(--surface-2); }

/* ============================================================================
   markdown body (shared by the note cards and the card preview)
   The renderer escapes first, so every rule below is about rhythm, not safety.
   ========================================================================== */
.md { font-size: 12.5px; line-height: 1.65; color: var(--ink-2); }
.md > *:first-child { margin-top: 0; }
.md > *:last-child { margin-bottom: 0; }
/* §3 — an empty note prints one muted line instead of a white rectangle: "no
   notes yet" is information, and blank space is not. */
.md-empty { color: var(--muted); font-size: 12.5px; margin: 0; }
.md h3, .md h4, .md h5, .md h6 {
  color: var(--ink); font-weight: 650; letter-spacing: -.005em;
  margin: 1.05em 0 .32em; line-height: 1.35;
}
.md h3 { font-size: 13.5px; }   /* `#` maps to h3, so this is the top level */
.md h4 { font-size: 13px; }
.md h5, .md h6 { font-size: 12.5px; color: var(--ink-2); }
.md p { margin: 0 0 .68em; }
.md ul, .md ol { margin: 0 0 .68em; padding-left: 1.35em; }
.md li { margin: .1em 0; }
.md li::marker { color: var(--muted); }
.md strong { color: var(--ink); font-weight: 650; }
.md em { font-style: italic; }
.md a { color: var(--accent); text-decoration: none; }
.md a:hover { text-decoration: underline; }
.md code {
  font-family: var(--font-mono); font-size: 11.5px;
  background: var(--surface-3); border-radius: 3px; padding: 1px 4px;
}
.md pre.md-code {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 10px 12px; margin: 0 0 .68em;
  font-family: var(--font-mono); font-size: 11.5px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word; overflow: auto;
}
.md blockquote {
  margin: 0 0 .68em; padding: 2px 0 2px 12px;
  border-left: 3px solid var(--border-strong); color: var(--ink-3);
}
.md blockquote p { margin: 0 0 .35em; }
.md hr { border: 0; border-top: 1px solid var(--border); margin: .9em 0; }

/* ============================================================================
   priority editor — one control, used by the database row, the event card and
   the dashboard row, so they can never disagree about the current value.
   ========================================================================== */
.seg-priority {
  display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm);
  overflow: hidden; background: var(--surface); vertical-align: middle;
}
.seg-priority button {
  flex: 1 1 auto; padding: 5px 10px; font-size: 11.5px; font-weight: 600; color: var(--muted);
  border-right: 1px solid var(--border); transition: background .12s, color .12s;
}
.seg-priority button:last-child { border-right: 0; }
.seg-priority button:hover { background: var(--surface-2); color: var(--ink-2); }
.seg-priority button.is-on-s { background: var(--accent); color: #fff; }
/* The Watch state has to read as "selected" at a glance next to the orange S —
   a barely-different grey was indistinguishable from the unselected state. */
.seg-priority button.is-on-watch {
  background: var(--watch-soft); color: var(--watch-ink); font-weight: 700;
  box-shadow: inset 0 0 0 1px var(--watch-line);
}
.seg-priority.is-compact { width: 100%; }
.seg-priority.is-compact button { padding: 5px 0; }

/* ============================================================================
   §9 — the following flag and the card's category row
   The flag is the personal shortlist: a small pennant that sits ahead of the
   event title, muted until it is raised, red once it is. The category row is
   the same segmented language as the S/Watch toggle beside it, so the card's
   head reads as one control strip.
   ========================================================================== */
.ev-follow {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--r-sm);
  color: var(--muted); transition: color .12s, background .12s;
}
.ev-follow:hover { color: var(--flag); background: var(--surface-2); }
.ev-follow.is-on { color: var(--flag); }
.ev-follow.is-on svg { fill: var(--flag); }
.ev-follow:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(200, 65, 15, .18); }
/* The row's flag sits inside a table cell, so it centres like everything else. */
.cell-flag .ev-follow { margin: 0 auto; }
/* V1.14 — the flag column's header cell is the Following SWITCH now: the same
   26px pennant the rows carry, and zero padding for the same reason the body
   cells have none (44px cell, 26px button, and the column's own padding would
   push it past its content box and paint a stray "..").
   V1.16 — the selector has to carry the SAME weight as `table.data thead th`
   (0,1,3) or it loses on source order and the fix silently never applies: the
   header cell kept its 11px padding, its `overflow: hidden` and its
   `text-overflow: ellipsis`, which is exactly the pair of dots that was still
   showing beside the pennant in the first row of the table. */
table.data thead th.col-flag {
  padding-left: 0; padding-right: 0;
  overflow: visible; text-overflow: clip;
}
/* The row-number column: a reading aid, so it is muted, tabular and allowed to
   be as narrow as a two-digit number plus its padding. */
table.data th.col-num { padding-left: 4px; padding-right: 4px; }
table.data td.cell-num {
  padding-left: 4px; padding-right: 4px;
  color: var(--muted); font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.th-flag { cursor: pointer; }

/* The event card's title row: the flag, then the name, kept centred as a unit. */
.ev-title-row {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-width: 0;
}
.ev-title-row .ev-title { min-width: 0; }
.seg-cats button { padding: 5px 9px; font-size: 11.5px; }
.seg-cats button.is-on { font-weight: 600; }
/* The Following filter is a single chip, flag + word, with its own on-state. */
.chip-follow { gap: 6px; height: 28px; }
.chip-follow .ico-flag { color: var(--muted); }
.chip-follow.is-on { background: var(--flag-soft); border-color: var(--flag-line); color: var(--flag); font-weight: 600; }
.chip-follow.is-on .ico-flag { color: var(--flag); fill: var(--flag); }

/* ============================================================================
   Radar (§2)
   Locked page: the view switch, the paging row and the axis are fixed; only the
   gantt body scrolls. All three views are the same Gantt — bar length is the
   real duration and the axis is the window.
   ========================================================================== */
.radar-page { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.radar-page .section-head { flex: 0 0 auto; }
/* §3 — one header row: summary on the left, the pager dead-centre, the view
   switcher on the right. The grid is `1fr auto 1fr` so the pager is centred on
   the ROW, not on whatever space the two sides leave. */
.radar-head {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 12px;
}
.radar-sum { justify-self: start; white-space: nowrap; }
.radar-head .head-actions { justify-self: end; margin-left: 0; }
.radar-stage { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

/* §2 — the three paging controls sit together, centred. */
.gantt-nav {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  gap: 8px;
}
.radar-head .gantt-nav { justify-self: center; }
.gantt-title {
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 3px 14px; border-radius: var(--r-sm); min-width: 210px;
  transition: background .12s;
}
.gantt-title:hover { background: var(--surface-2); }
.gantt-title b { font-size: 14px; font-weight: 650; letter-spacing: -.01em; }
.gantt-title span { font-size: 11px; color: var(--muted); }
/* The bare arrow glyph needs a little more size to read as an arrow rather than
   as a dash at 12px. */
.gantt-nav .btn { font-size: 15px; line-height: 1; min-width: 40px; padding: 0 8px; }

.gantt {
  position: relative;   /* §7 — the drag handle is positioned against this box */
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-1);
}
.g-head, .g-row { display: grid; grid-template-columns: var(--g-label) var(--g-cols); }

/* §7 — the draggable seam. Titles in the label column are ellipsised, and no
   fixed width can be right for every title, so the column is adjustable. The
   handle sits OVER the seam rather than in a grid track of its own: the bars are
   placed with `grid-column: 2 + startIdx`, so an extra track would shift every
   bar one column right and quietly desynchronise the bars from the axis.
   It is 10px wide and 6px left of the seam, so it is a large target for the
   pointer while intruding only 4px into the day area — the bars have no bar
   there to steal a click from. At rest it draws nothing (the label column's own
   border is the seam); hovering and dragging bring up an accent line. */
.g-grip {
  position: absolute; top: 0; bottom: 0; z-index: 4;
  left: calc(var(--g-label) - 6px); width: 10px;
  cursor: col-resize; touch-action: none;
  display: flex; align-items: center; justify-content: center;
}
.g-grip::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 5px; width: 1px;
  background: transparent; transition: background .12s, width .12s, left .12s;
}
.g-grip:hover::before, .g-grip.is-dragging::before { background: var(--accent); width: 2px; left: 4px; }
.g-grip i {
  width: 3px; height: 26px; border-radius: 2px; background: var(--accent);
  opacity: 0; transition: opacity .12s;
}
.g-grip:hover i, .g-grip.is-dragging i, .g-grip:focus-visible i { opacity: 1; }
.g-grip:focus-visible { outline: none; }
/* ONE scroll container (§2). If only the row area scrolled, its vertical
   scrollbar would narrow the row grid relative to the header grid and every bar
   would drift left of its own date column — the header is sticky instead, so
   both grids always share the same width. */
.g-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.g-head {
  position: sticky; top: 0; z-index: 3; flex: 0 0 auto;
  background: var(--surface-2); border-bottom: 1px solid var(--border);
}
.g-corner { padding: 8px 14px; border-right: 1px solid var(--border); }
.g-day {
  padding: 6px 2px 5px; text-align: center; border-left: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: center; gap: 1px; overflow: hidden;
}
.g-day .g-dow { font-size: 9.5px; color: var(--muted); letter-spacing: .04em; text-transform: uppercase; }
.g-day b { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.g-day.is-weekend { background: rgba(20, 18, 15, .035); }
.g-day.is-today { background: var(--accent); }
.g-day.is-today .g-dow, .g-day.is-today b { color: #fff; }

/* §2 — past ~16 columns there is no room for "Tue 6" on every day. The axis
   keeps only the day numbers, but today keeps its weekday label, because
   "where am I" is the one question a 31-digit header cannot otherwise answer. */
.gantt.is-narrow .g-day { padding: 6px 1px 5px; }
.gantt.is-narrow .g-day .g-dow { display: none; }
.gantt.is-narrow .g-day b { font-size: 10.5px; }
.gantt.is-narrow .g-day.is-today { padding: 4px 1px 3px; }
.gantt.is-narrow .g-day.is-today .g-dow {
  display: block; font-size: 8px; letter-spacing: .06em; opacity: .92;
}

.g-rows { flex: 1 1 auto; display: flex; flex-direction: column; }
/* The weekend / today bands are painted by one generated gradient across the
   track area only (hence the sized, offset background layer). */
.g-row {
  align-items: center; min-height: 34px; border-bottom: 1px solid var(--border);
  background-image: var(--band);
  background-size: calc(100% - var(--g-label)) 100%;
  background-position: var(--g-label) 0;
  background-repeat: no-repeat;
}
.g-row:last-child { border-bottom: 0; }
.g-row:hover { background-color: #FCFBF9; }

.g-label {
  display: flex; align-items: center; gap: 9px; text-align: left;
  padding: 5px 12px 5px 14px; min-width: 0;
  border-right: 1px solid var(--border);
}
.g-label:hover .g-label-name { color: var(--accent); }
.g-label-bar { flex: 0 0 3px; width: 3px; height: 24px; border-radius: 2px; background: var(--border-strong); }
.g-label-bar.s { background: var(--accent); }
.g-label-bar.w { background: var(--watch-strong); }
.g-label-txt { min-width: 0; display: flex; flex-direction: column; }
.g-label-name {
  font-size: 12.5px; font-weight: 600; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.g-label-meta { font-size: 10.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.g-bar {
  grid-row: 1 / -1; align-self: center; height: 24px; margin: 0 1px;
  border-radius: 4px; background: var(--border-strong); overflow: hidden;
  display: flex; align-items: center; padding: 0 8px; min-width: 0;
  transition: filter .12s;
}
.g-bar:hover { filter: brightness(.94); }
.g-bar.s { background: var(--accent); }
.g-bar.s .g-bar-txt { color: #fff; }
.g-bar.w { background: var(--watch); }
/* A bar that starts before / ends after the window is squared off on that side
   so a clipped bar is visibly not the whole event. */
.g-bar.clip-l { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.g-bar.clip-r { border-top-right-radius: 0; border-bottom-right-radius: 0; }
/* One day is ~14px at month scale — a caption there would be a smear of ink. */
.g-bar.is-short { padding: 0 4px; }
.g-bar.is-short .g-bar-txt { display: none; }
.g-bar-txt {
  /* Must be a block-level flex item: `text-overflow` does nothing on a flex
     container, which is why the bar text used to be sliced mid-glyph. */
  display: block; flex: 1 1 auto; min-width: 0;
  font-size: 11px; font-weight: 600; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.g-bar-txt em { font-style: normal; font-weight: 500; font-size: 10px; opacity: .7; margin-left: 6px; }

/* ============================================================================
   modal — the centred event card (§3)
   ========================================================================== */
.scrim {
  position: fixed; inset: 0; background: rgba(20,18,15,.32);
  z-index: 90; display: flex; align-items: center; justify-content: center;
  padding: 24px; animation: fade .14s ease-out;
}
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes rise { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }

.modal {
  background: var(--surface); border-radius: var(--r-lg); box-shadow: var(--shadow-3);
  width: min(680px, 100%); max-height: 86vh; display: flex; flex-direction: column;
  animation: rise .16s ease-out;
}
.modal.wide { width: min(880px, 100%); }
.modal-head {
  padding: 15px 20px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 12px;
}
.modal-head h3 { font-size: 15px; font-weight: 650; }
.modal-head .hint { font-size: 11.5px; color: var(--muted); }
.modal-head .x { margin-left: auto; font-size: 18px; color: var(--muted); line-height: 1; flex: 0 0 auto; }
.modal-head .x:hover { color: var(--ink); }
.modal-body { padding: 18px; overflow: auto; }
.modal-foot {
  padding: 13px 20px; border-top: 1px solid var(--border);
  display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; align-items: center;
}
.modal-foot .left { margin-right: auto; }

/* §3 — the record card. Every value the user owns is edited where it is read,
   so there is no Actions module and no Status module: the field IS the control.
   §3 (V1.7) — the card is sized by its CONTENTS now, not in vh. A vh-tall card
   was mostly empty space whenever the right column was shorter than the frame;
   the height is now exactly head + why-it-matters + the Notes default. */
.ev-modal {
  width: min(1080px, 100%);
  height: auto;
  max-height: calc(100vh - 32px);
}
.ev-head { align-items: flex-start; }
.ev-head-main {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  text-align: center;
}
.ev-title { font-size: 21px; font-weight: 650; letter-spacing: -.02em; line-height: 1.25; }
.ev-tags { display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap; }

/* The two columns. Facts and the description on the left (the side that can
   overflow), the two writers on the right. Only the left column may scroll, so
   the editor never moves. */
.ev-body {
  flex: 0 1 auto; min-height: 0; overflow: auto;
  display: grid; gap: 22px; padding: 17px 20px;
  grid-template-columns: minmax(300px, 392px) minmax(0, 1fr);
}
.ev-col { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.ev-col-info { overflow-y: auto; gap: 11px; padding-right: 4px; }
.ev-col-write { gap: 14px; }
.ev-block { display: flex; flex-direction: column; min-height: 0; }
.ev-block-head { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
.ev-block-head .hint { font-size: 11px; color: var(--muted); }

/* §2 — the description writer. Plain text, not markdown: this is the line the
   Database table prints under the event name, so it stays a single short
   paragraph by design. It sits above the facts because it is the one field on
   the left that is prose, and its size is deliberately tight — the card has to
   fit eight facts, this box and the notes editor without scrolling at 1366x768. */
.ev-desc-block { flex: 0 0 auto; }

.ev-facts { display: grid; grid-template-columns: 1fr; gap: 10px; }
.ev-f { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.ev-k {
  font-size: 10px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}

.ef {
  height: 30px; width: 100%; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); padding: 0 9px;
  font-size: 12.5px; color: var(--ink); outline: 0;
  transition: border-color .12s, box-shadow .12s, background .12s;
}
.ef:hover { border-color: var(--border-strong); }
.ef:focus { border-color: var(--border-strong); box-shadow: 0 0 0 3px rgba(20,18,15,.04); }
.ef.mono { font-family: var(--font-mono); font-size: 12px; }
/* The read-only state has to look deliberate, not broken. */
.ef[readonly], .ef[disabled] {
  background: var(--surface-2); color: var(--ink-3); cursor: default;
}
.ef[readonly]:hover, .ef[disabled]:hover { border-color: var(--border); }

.ev-duo { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ev-duo > .ef { flex: 1 1 auto; }
.ev-duo .sep { color: var(--muted); flex: 0 0 auto; font-size: 12px; }
.ev-duo .btn { flex: 0 0 auto; }

/* §3 — `.seg-choice` (the Content-status and Data-quality chip rows) is gone
   with its two fields. `.seg-priority` below is the only chip row left. */

/* §2 — several official links per event. */
.ev-links { display: flex; flex-direction: column; gap: 6px; }
.ev-link-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.ev-link-row .ef { flex: 1 1 auto; min-width: 0; }

.ev-add-link { align-self: flex-start; }
.btn.is-icon {
  width: 28px; padding: 0; flex: 0 0 auto;
  font-size: 15px; line-height: 1; color: var(--muted);
}
.btn.is-icon:hover { color: var(--danger); border-color: var(--border-strong); }

/* §3 — the notes block sets the card's height, so it owns an explicit default
   (`--ev-notes-h`) instead of "whatever is left over". It still grows if the
   left column turns out taller, which is what keeps the two sides flush; the
   number is here rather than inside the layout because it is the one value that
   decides how tall the card is. */
.ev-notes { border-top: 1px solid var(--border); padding-top: 14px; }
.ev-notes, .ev-notes .md-input, .ev-notes .md-preview { flex: 1 1 auto; min-height: 0; }
.ev-notes .md-input, .ev-notes .md-preview { overflow-y: auto; }
.ev-notes .md-preview { min-height: var(--ev-notes-h, 150px); }
.md-toggle { margin-left: auto; flex: 0 0 auto; }

textarea.ta {
  width: 100%; min-height: 66px; resize: vertical;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); padding: 10px 12px; font-size: 13px;
  line-height: 1.6; outline: 0;
  transition: border-color .12s, box-shadow .12s;
}
textarea.ta:focus { border-color: var(--border-strong); box-shadow: 0 0 0 3px rgba(20,18,15,.035); }
textarea.ta::placeholder { color: #B4B0A8; }
/* §3 — description and why-it-matters sit in the same column now and both
   default to exactly two lines of text. Capped, not merely floored: the card has
   to fit its two columns, an editor and a preview without scrolling at 1366x768,
   and a box that grows with its content makes that a hope instead of a rule. */
.ev-desc, .ev-why { min-height: 63px; max-height: 63px; resize: none; }
/* Markdown reads better in mono — it keeps the `**` pairs vertically aligned
   with the prose they wrap. */
textarea.md-input {
  min-height: 140px; font-family: var(--font-mono);
  font-size: 12.5px; line-height: 1.65;
  resize: none;
}
textarea.md-input[readonly] { background: var(--surface-2); color: var(--ink-3); }

/* §6 — the reading surface is paper, not grey. The old `--surface-2` slab was a
   textarea pretending to be a document: a big flat grey rectangle that made the
   note look disabled. On paper it reads as the page you write on, and it is the
   same material as the card in the grid, so opening a card does not change what
   the note is made of. */
.md.md-preview {
  padding: 10px 12px;
  border: 1px solid var(--paper-line); border-radius: var(--r-sm);
  background: var(--paper);
}
.md-preview[hidden] { display: none; }

/* §3 — the boxed Why-it-matters block. Tinted and badged with a bulb, so it
   reads as reference material rather than as the first paragraph of the note
   below it.
   §6 — `is-flat` drops the label and tightens the type: on a note card the box
   only has a couple of lines to work with, and a heading was spending one. */
.why-box {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  border-radius: var(--r-md); padding: 10px 12px;
}
.why-ico { color: var(--accent); display: flex; align-items: center; padding-top: 1px; }
.why-txt { min-width: 0; }
.why-k {
  display: block; margin-bottom: 2px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
}
.why-txt p { font-size: 12.5px; line-height: 1.6; color: var(--ink-2); margin: 0; }

.why-box.is-flat {
  padding: 9px 11px; border: 0; border-radius: var(--r-sm); gap: 8px;
  background: var(--paper-2);
}
/* §9 — on a NOTE CARD the why-line hugs its own text and centres, instead of
   stretching to the card's full width and leaving a big blank either side. A
   long sentence still fills the width (fit-content falls back to max-content up
   to the available room); only a short one tightens up. The dialog's copy is
   deliberately left alone — there it is a full-width pull-quote. */
.note-card .why-box.is-flat { width: fit-content; max-width: 100%; margin-inline: auto; }
.why-box.is-flat .why-ico { padding-top: 0; }
.why-box.is-flat .why-txt p {
  font-size: 12px; line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}

pre.code {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 14px; font-size: 12px; line-height: 1.65;
  font-family: var(--font-mono); white-space: pre-wrap; word-break: break-word;
  max-height: 52vh; overflow: auto; margin: 0;
}

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-field { display: flex; flex-direction: column; gap: 5px; }
.form-field.full { grid-column: 1 / -1; }
.form-field label { font-size: 11.5px; font-weight: 600; color: var(--ink-3); }
.form-field input, .form-field select, .form-field textarea {
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 7px 10px; font-size: 13px; outline: 0; background: var(--surface); width: 100%;
}
.form-field input:focus, .form-field select:focus, .form-field textarea:focus { border-color: var(--border-strong); }

.toast-host {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
  z-index: 120; display: flex; flex-direction: column; gap: 8px; align-items: center;
  pointer-events: none;
}
.toast {
  background: var(--ink); color: #fff; font-size: 12.5px; font-weight: 500;
  padding: 9px 16px; border-radius: 99px; box-shadow: var(--shadow-2);
  animation: rise .16s ease-out;
}
.toast.ok { background: #14532D; }
.toast.err { background: #8C2118; }

.drawer-toggle { display: none; }

/* ============================================================================
   responsive
   Priority order: Desktop ★★★★★ · Tablet ★★★ · Mobile ★★.
   ========================================================================== */
@media (max-width: 1300px) {
  :root { --g-label: 198px; }
}

/* Not enough room for the locked layouts — fall back to plain page scrolling
   rather than crushing the radar column / gantt rows to nothing. */
@media (max-width: 1160px), (max-height: 660px) {
  .view.is-lock { overflow-y: auto; padding-bottom: 56px; }
  .view.is-lock .view-inner { display: block; height: auto; }
  .dash, .radar-page { display: block; }
  /* §1 — both bands stop being grids and simply stack: timeline, topic bars,
     events, Following. Nothing to drag when they are stacked, so the seam goes. */
  .dash-top { display: block; height: auto; }
  .dash-tl { margin-bottom: 14px; }
  .dash-topic { margin-bottom: 16px; }
  .dash-grip { display: none; }
  .dash-bottom { display: block; }
  .dash-events { margin-bottom: 16px; }
  /* Panels revert to plain flow, but the gantt axis keeps sticking to the top
     of the page while its rows scroll. */
  .panel, .gantt, .g-scroll, .g-rows { display: block; }
  .panel-scroll, .panel-fill, .g-scroll { overflow: visible; }
  .panel-head { position: static; }
  .panel { overflow: hidden; }
  .g-head { position: sticky; top: 0; z-index: 3; }
  /* §5 — the two-row grid needs a definite height to halve, and this fallback
     is exactly where that height stops existing. So the grid goes back to
     natural rows and the card back to a height of its own. */
  .notes-grid {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-auto-rows: auto; overflow: visible;
  }
  .note-card { height: 400px; }
}

@media (max-width: 1024px) {
  :root { --g-label: 176px; --content-max: 100%; }
  .topbar { grid-template-columns: auto 1fr auto; gap: 10px; padding: 0 14px; }
  .brand-txt i { display: none; }
  .nav { justify-self: end; }
  .nav-item span { display: none; }
  .nav-item { padding: 0 12px; }
  .saved { display: none; }
  .notes-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
}

@media (max-width: 820px) {
  :root { --g-label: 140px; --tl-h: 84px; }
  /* §7 (V1.16) — the label column is draggable on a phone too. It used to be
     hidden here because the mobile ladder fixes its own 112px column, but a
     phone is exactly where titles are cut shortest, so the seam stays and the
     clamp (the axis never gets less than 45%) is what keeps it sane. */
  body { font-size: 13.5px; }
  .topbar { height: 50px; }
  .view { padding: 16px 14px 60px; }
  .section-head { flex-wrap: wrap; }
  .section-head .head-actions { margin-left: 0; width: 100%; }
  .form-grid { grid-template-columns: 1fr; }

  /* table -> read-only cards */
  .table-wrap { display: none; }
  .card-list { display: flex; }

  /* The selectors module hides on a phone; the actions module keeps a Filters
     button so the drawer is still reachable. What is left of it is Filters and
     Reset, and the base 2×2 grid already puts those side by side — the two
     import / export controls are desktop-only like the top bar's pair. */
  .db-bar { grid-template-columns: 1fr; }
  .db-mod-filters { display: none; }
  .drawer-toggle { display: inline-flex; }

  /* Mobile is read-only: every create / import / export control disappears;
     `!important` is needed because a few of these carry inline display values
     set for the desktop layout. */
  .desktop-only-inline { display: none !important; }

  /* §1 / §9 (V1.16) — the phone order is timeline · Topic Distribution ·
     Following · events. The desktop seam (events | Following) has nothing to
     divide at 390px — it is already a stack — so the two lists are simply put
     in the order that matters when you are holding the phone: the shortlist
     first, the whole list last. `order` rather than reordering the markup, so
     desktop keeps its two columns without a second copy of either panel. */
  .dash-bottom { display: flex; flex-direction: column; }
  .dash-follow { order: 1; margin-bottom: 16px; }
  .dash-events { order: 2; margin-bottom: 0; }

  /* §2 (V1.16) — four rows to a screen. The panel is exactly one screen tall
     (under the sticky 50px bar) and the list divides it into four, which is
     what "one screen of event cards" means on a device that cannot show more
     than a handful of them. `100svh` is the small-viewport height, so a phone
     whose URL bar is showing does not push the fourth card out of view. */
  .dash-events .panel { display: flex; flex-direction: column; height: calc(100vh - 58px); height: calc(100svh - 58px); }
  .dash-events .panel-scroll { overflow-y: auto; }
  .radar-list { height: 100%; grid-auto-rows: calc(100% / 4); }

  /* §5 (V1.16) — a note card is as tall as its own text and no taller: the fixed
     400px card was half empty on a short note and clipped a long one's title.
     Every clamp inside it goes with the fixed height — the brief is "show all of
     it", so the title may run to as many lines as it needs and the note may
     scroll the page rather than itself. */
  .notes-grid { grid-auto-rows: auto; overflow: visible; }
  .note-card { height: auto; }
  .nc-name { display: block; -webkit-line-clamp: none; overflow: visible; }
  .nc-notes { min-height: 0; overflow: visible; }

  /* §1 — the timeline keeps its geometry but drops the weekday, which a 24px
     column cannot hold; the pager and selector stack instead of squeezing. */
  /* §1 — on a phone the head unstacks into three centred rows: the count, the
     pager, the window selector. The summary is kept (it is one number now);
     only the day-count and the "busy" tail were dropped, and those are gone
     everywhere, not just here. */
  .tl-head { display: flex; flex-wrap: wrap; gap: 10px; }
  .tl-count { order: 1; width: 100%; }
  .tl-nav { order: 2; width: 100%; justify-content: center; margin-inline: 0; }
  .tl-zoom { order: 3; margin-left: 0; width: 100%; }
  .tl-zoom button { flex: 1 1 auto; padding: 6px 0; }
  .tl-range b { font-size: 12.5px; }
  .tl-tick { font-size: 9px; }
  .tl-dow { display: none; }

  /* the radar row stacks: three columns at 390px squeezed the event name */
  .radar-row { grid-template-columns: 1fr; gap: 7px; }
  .radar-actions { flex-direction: row; align-items: center; flex-wrap: wrap; }

  /* the gantt drops the weekday labels and the in-bar captions; the left
     column already names every row */
  /* §3 — on a phone the header row unstacks back into a plain column: summary,
     pager, view switcher, one under the other. */
  .radar-head { display: block; }
  .radar-head .gantt-nav { margin: 8px 0 10px; }
  .radar-head .head-actions { margin-top: 2px; }
  .gantt-nav { flex-wrap: wrap; }
  .gantt-title { min-width: 0; flex: 1 1 auto; }
  .g-day { padding: 5px 1px; }
  .g-day .g-dow { display: none; }
  .g-day b { font-size: 9.5px; }
  .gantt .g-bar-txt { display: none; }
  .g-bar { height: 18px; margin: 0; border-radius: 3px; }
  .g-row { min-height: 30px; }
  .g-label { padding: 5px 6px 5px 8px; gap: 6px; }
  .g-label-name { font-size: 11px; }
  .g-label-bar { height: 20px; }
  .g-label-meta { display: none; }

  /* §7 (V1.16) — frozen panes, the way a spreadsheet does them, and the reason
     the hand-off between the two freeze axes has to be spelled out:
       · the day numbers are frozen to the top (`.g-head` is sticky, as before);
       · the title column is frozen to the left, and it is frozen in the same
         scrolling box, so a title never slides away from its own bar;
       · the corner cell belongs to both, so it takes the highest layer.
     `min-width` is what makes the horizontal scroll exist at all: the axis may
     never be squeezed below 18px a day, so dragging the label column wide pushes
     the rows past the viewport and the pane freeze starts to mean something. At
     the default 112px column neither week nor 2 weeks overflows, so nothing
     about the phone's default view changes. */
  .g-scroll { overflow: auto; }
  .g-head, .g-row { min-width: calc(var(--g-label) + var(--n) * 18px); }
  .g-label {
    position: sticky; left: 0; z-index: 2;
    background: var(--surface);
  }
  .g-row:hover .g-label { background: #FCFBF9; }
  .g-corner { position: sticky; left: 0; z-index: 5; background: var(--surface-2); }

  /* §3 / §4 — the card becomes a single scrolling column. It is read-only on
     phones, so "everything visible without scrolling" has nothing to protect
     here; letting the body scroll is the only sane thing to do at 390px. */
  .scrim { padding: 12px; align-items: flex-start; }
  .modal, .modal.wide { width: 100%; max-height: 92vh; }
  .ev-modal { width: 100%; height: auto; max-height: 92vh; }
  .modal-head { padding: 13px 15px; }
  .modal-body { padding: 15px; }
  .ev-body {
    display: block; overflow-y: auto; padding: 15px;
    grid-template-columns: 1fr;
  }
  .ev-col-info { overflow: visible; padding-right: 0; margin-bottom: 15px; }
  .ev-col-write { gap: 12px; }
  .modal-foot { padding: 12px 15px; }
  .ev-title { font-size: 17px; }
  .ev-facts { gap: 12px; }
  /* §3 — on a phone the card scrolls anyway, so the two writers may grow. */
  .ev-desc, .ev-why { max-height: none; }

  /* §3 — one card per row, and the date range gets its own line. */
  .notes-head .hint { flex: 1 1 100%; }
  .notes-search { width: 100%; }
  .notes-head .head-actions { width: 100%; margin-left: 0; }
  .range { width: 100%; }
  .range .input-date { flex: 1 1 auto; min-width: 0; }
  .notes-grid { grid-template-columns: 1fr; }
  /* §5 (V1.16) — one card per row, and every card is exactly as tall as its own
     text. The fixed 396px this block used to set is gone: it is the last rule
     that would have won the tie over `height:auto` above, and the brief is
     "card height follows the content, showing all of it" — a fixed height does
     the opposite in both directions on a 390px column. */
  .note-card { height: auto; }

  /* §4 — the note dialog has no margin rule to pull in any more; it just gives
     the footer a little more room on a narrow screen. */
  .modal.note-modal { width: 100%; }
  .modal-head.nm-head { padding-left: 16px; padding-right: 16px; }
  .modal-body.nm-body { padding-left: 16px; padding-right: 16px; }
  .modal-foot.nm-foot { padding-left: 16px; }
  .nm-note .md-input, .nm-note .md-preview { min-height: 180px; }
}

@media (max-width: 480px) {
  :root { --g-label: 112px; }
  .brand-txt b { font-size: 14px; }
  /* A URL field plus Open plus remove does not fit on one 360px line. */
  .ev-link-row { flex-wrap: wrap; }
  .ev-link-row .ef { flex: 1 1 100%; }
  .why-box { gap: 7px; padding: 9px 10px; }
}
