:root {
  color-scheme: light;
  --accent: #6366f1;
  --accent-dark: #4f46e5;
  --accent-tint: #eef0ff;
  --bg: #f4f4f7;
  --card-bg: #ffffff;
  --text: #1f2330;
  --muted: #6b7280;
  --border: #e5e7eb;
  --tab-strip-bg: #e4e5ea;
  --tab-chip-active-bg: #ffffff;
  --checkbox-border: #9ca3af;
  /* One height for every control on the meeting metadata row. */
  --meeting-field-h: 2.35rem;
  --danger: #dc2626;
  --danger-tint: #fef2f2;
  --success: #16a34a;
  --success-tint: #ecfdf5;
  --warn: #d97706;
  --warn-tint: #fffbeb;
  --neutral-tint: #f3f4f6;
  --radius: 14px;
  --shadow: 0 2px 10px rgba(20, 20, 40, 0.06);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* Dark mode: applied automatically when the OS prefers it, or forced
   either way by data-theme (set by the toggle in the topbar). The
   data-theme rules come after the media query so they win regardless of
   source order — an attribute selector is simply more specific than a
   bare :root, media query or not. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --accent: #818cf8;
    --accent-dark: #a5b4fc;
    --accent-tint: #2b2c47;
    --bg: #15161c;
    --card-bg: #1e1f27;
    --text: #e8e9ed;
    --muted: #9298a8;
    --border: #34353f;
    --tab-strip-bg: #0e0f14;
    --tab-chip-active-bg: #1e1f27;
    --checkbox-border: #6b7280;
    --danger: #f87171;
    --danger-tint: #3a2226;
    --success: #4ade80;
    --success-tint: #1c3327;
    --warn: #fbbf24;
    --warn-tint: #3a2f18;
    --neutral-tint: #292a33;
    --shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --accent: #6366f1;
  --accent-dark: #4f46e5;
  --accent-tint: #eef0ff;
  --bg: #f4f4f7;
  --card-bg: #ffffff;
  --text: #1f2330;
  --muted: #6b7280;
  --border: #e5e7eb;
  --tab-strip-bg: #e4e5ea;
  --tab-chip-active-bg: #ffffff;
  --danger: #dc2626;
  --danger-tint: #fef2f2;
  --success: #16a34a;
  --success-tint: #ecfdf5;
  --warn: #d97706;
  --warn-tint: #fffbeb;
  --neutral-tint: #f3f4f6;
  --shadow: 0 2px 10px rgba(20, 20, 40, 0.06);
}
/* FR#144: light mode only — swaps which surfaces read as "sidebar" vs
   "content pane". Both sidebars go gray (--bg) and the content pane
   between them goes white (--card-bg), the reverse of how --bg/--card-bg
   pair up everywhere else. Overrides just these three structural
   elements rather than swapping the variables' own values, so cards,
   buttons, and everything else keep the exact light/dark relationship
   they already had — dark mode is untouched either way. */
:root[data-theme="light"] .topbar { background: var(--bg); }
:root[data-theme="light"] .right-panel { background: var(--bg); }
:root[data-theme="light"] .view-root { background: var(--card-bg); }
:root[data-theme="dark"] {
  color-scheme: dark;
  --accent: #818cf8;
  --accent-dark: #a5b4fc;
  --accent-tint: #2b2c47;
  --bg: #15161c;
  --card-bg: #1e1f27;
  --text: #e8e9ed;
  --muted: #9298a8;
  --border: #34353f;
  --tab-strip-bg: #0e0f14;
  --tab-chip-active-bg: #1e1f27;
  --checkbox-border: #6b7280;
  --danger: #f87171;
  --danger-tint: #3a2226;
  --success: #4ade80;
  --success-tint: #1c3327;
  --warn: #fbbf24;
  --warn-tint: #3a2f18;
  --neutral-tint: #292a33;
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

/* iOS renders the text and glyphs of an unstyled <button> in its own system
   blue, and nothing here said otherwise — so on a phone the undo arrow, the
   "Aa", the list and the link button all came out blue, while the buttons
   whose icons carry an explicit stroke stayed dark. On desktop the default
   is dark, which is why the design never showed it. Saying "follow the text
   around you" makes the phone match what everything was drawn against.
   Buttons with a deliberate colour of their own (.small, .ghost, .danger,
   the toolbar's active state) all set it later and are unaffected. */
button {
  color: inherit;
}

/* Taking the colour off the UA also took its greying-out of disabled
   buttons with it, which would have left "nothing to undo" looking just as
   available as anything else. Fading the whole button says the same thing
   independently of whatever colour it happens to be. */
button:disabled {
  opacity: 0.35;
  cursor: default;
}

/* FR#347: undo and redo are the one pair that should stand out from the
   chrome — they're the way back from a mistake. Purple, the app's own
   accent, rather than the blue that was only ever an iOS default. Disabled
   keeps the muted look, so "nothing to undo" still reads as unavailable. */
#undo-btn:not(:disabled),
#redo-btn:not(:disabled),
#undo-history-btn { color: var(--accent); }

/* Native form controls don't inherit page colors on their own — without
   this every input/select/textarea stays browser-default white-on-black
   regardless of theme. Excludes checkbox/radio, which are small native
   controls that shouldn't get box/border/padding treatment. */
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.7rem;
  font-size: 0.9rem;
  font-family: inherit;
}
input:not([type="checkbox"]):not([type="radio"]):focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.hidden { display: none !important; }
.muted { color: var(--muted); }

/* ---------- Login / setup screens ---------- */
.login-screen {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.login-card {
  background: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 2rem;
  max-width: 380px;
  width: 100%;
  text-align: center;
}
.login-logo { font-size: 3rem; }
.login-card h1 { margin: 0.3rem 0 0.2rem; font-size: 1.4rem; }
.login-card input {
  width: 100%;
  margin-top: 1.2rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 1rem;
}
.login-card button, .button-primary {
  width: 100%;
  margin-top: 1rem;
  padding: 0.75rem;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 10px;
  font-size: 1rem;
  cursor: pointer;
}
.login-card button:active, .button-primary:active { background: var(--accent-dark); }
.error-text { color: var(--danger); margin-top: 0.8rem; font-size: 0.9rem; }

/* ---------- App shell ---------- */
.app { min-height: 100vh; padding-bottom: calc(70px + var(--safe-bottom)); }

.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.8rem 1.2rem;
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar-brand { font-weight: 700; font-size: 1.1rem; display: flex; gap: 0.4rem; align-items: center; cursor: pointer; }
/* FR#385: the app's name shows on a phone (where the bar is the only thing
   identifying the app) and not on a desktop (where the whole sidebar is
   already unmistakably it, and the space is better spent on the icon
   alone). FR#373 had it the other way round. */
@media (min-width: 860px) {
  .topbar-brand-text { display: none; }
}
/* FR#296: desktop never shows this regardless of the .hidden toggle below —
   only unhidden inside the mobile breakpoint further down. */
.topbar-back-btn { display: none; background: none; border: none; cursor: pointer; padding: 0; margin-right: -0.6rem; font-size: 1.3rem; line-height: 1; color: var(--text); position: relative; }
/* FR#333: the arrow itself is a thin glyph and was a correspondingly thin
   target. This pads the touchable area out to roughly a fingertip without
   moving the arrow or changing the height of the bar — a pseudo-element
   takes no part in layout, so nothing around it shifts. */
.topbar-back-btn::after {
  content: '';
  position: absolute;
  inset: -14px -16px;
}
/* FR#334/#335: a tap needs to look like it landed. There's no hover on a
   touch screen, so :active is the only moment available — and it has to be
   instant going down (a transition here would delay the very feedback it
   is meant to give) while fading back on release. */
.topbar-back-btn:active,
.bottom-nav .nav-btn:active {
  opacity: 0.4;
  transition: none;
}
.topbar-back-btn,
.bottom-nav .nav-btn { transition: opacity 0.18s ease; }
/* FR#278: populated on every render() regardless of viewport (see
   updateTopbarPageTitle in app.js) but only ever shown on mobile, in place
   of the "Project Manager 2000" text — see the mobile-only swap further
   down. Hidden here as the default/desktop state. */
.topbar-page-title { display: none; }

/* FR#349: a few buttons need a shorter word on a narrow screen. Both are in
   the markup and CSS picks one, so there's no JS re-render on resize. */
.login-remember {
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  gap: 0.45rem;
  margin-top: 0.9rem;
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.9rem;
  cursor: pointer;
  user-select: none;
}
/* .login-card input is width:100% for the email/password fields, which a
   checkbox must not inherit — stretched full width it shoved the label off
   to the side and wrapped it onto two lines. */
.login-remember input {
  margin: 0;
  width: auto;
  flex: none;
  margin-top: 0;
}

.label-short { display: none; }
.label-long { display: inline; }
.tabbar { display: none; gap: 0.3rem; margin-left: 1rem; flex: 1; }
.sidebar-search { display: none; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 0.6rem; }

.nav-btn {
  background: none;
  border: none;
  padding: 0.5rem 0.8rem;
  border-radius: 10px;
  cursor: pointer;
  font-size: 0.95rem;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.nav-btn.active { background: var(--accent-tint); color: var(--accent-dark); font-weight: 600; }
.nav-btn:hover { background: var(--border); }
.nav-btn.active:hover { background: var(--accent-tint); }
/* FR#36: hand-drawn line icons (see NAV_ICONS in app.js) replacing emoji in
   the nav menus — sized in em so they scale with whichever button's
   font-size they land in (sidebar vs bottom-nav vs sub-items) same as the
   emoji they replaced did. */
.nav-icon, .theme-toggle-icon { display: inline-flex; flex-shrink: 0; }
.nav-icon svg, .theme-toggle-icon svg { width: 1.15em; height: 1.15em; display: block; }
.sidebar-favorite-btn { padding-left: 1.6rem; font-size: 0.88rem; }
/* FR#239: same red today-count badge language as .project-today-badge on
   the cards, adapted for an inline flex row instead of a card overlay —
   margin-left:auto pins it to the button's right edge regardless of how
   long the project name runs. */
.sidebar-today-badge {
  margin-left: auto;
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: #ef4444;
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* ---------- Sidebar nav-groups: Projects/Tools/Settings expand-collapse (FR#49) ---------- */
.nav-group-header { display: flex; align-items: center; }
.nav-group-header .nav-group-main { flex: 1; min-width: 0; }
.nav-group-main { width: 100%; text-align: left; }
.nav-group-chevron {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0.3rem 0.5rem;
  flex-shrink: 0;
  font-size: 0.8rem;
  transition: transform 0.2s ease;
}
.nav-group-chevron-inline { margin-left: auto; font-size: 0.8rem; color: var(--muted); transition: transform 0.2s ease; }
.nav-group.expanded .nav-group-chevron,
.nav-group.expanded .nav-group-chevron-inline { transform: rotate(180deg); }
.nav-group-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.25s ease;
}
.nav-group.expanded .nav-group-body { max-height: 800px; }
.nav-sub-btn { padding-left: 1.6rem; font-size: 0.88rem; width: 100%; text-align: left; }

.icon-btn {
  background: none;
  border: none;
  /* FR#347: without an explicit colour, iOS renders a <button> in its own
     system blue while desktop browsers use the default dark button text —
     which is why the undo arrow looked blue on the phone and dark
     everywhere I could test it. Stated outright so both agree. */
  color: var(--text);
  font-size: 1.2rem;
  cursor: pointer;
  padding: 0.3rem 0.5rem;
  border-radius: 8px;
}
.icon-btn:hover { background: var(--bg); }
.icon-btn:active { background: var(--border); }
.icon-btn.active { background: var(--accent-tint); }
.undo-history-wrap { position: relative; display: flex; align-items: center; }
.history-arrow { padding: 0.3rem 0.15rem; font-size: 0.6rem; margin-left: -0.35rem; }
/* FR#125: position:fixed (JS-set left/top or bottom, see positionFloatingPanel)
   rather than position:absolute — this button sits low in the sidebar, so
   top:100% could put most of a tall history list below the viewport (and
   the sidebar clips overflow besides, same reasoning as FR#101). */
.undo-history-dropdown {
  position: fixed;
  width: 260px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.4rem;
  z-index: 100;
}
.undo-history-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0.4rem 0.6rem;
  border-radius: 6px;
  font-size: 0.82rem;
  cursor: pointer;
  color: var(--text);
}
.undo-history-item:hover { background: var(--bg); }
.undo-history-item.future { color: var(--muted); }
.undo-history-item.current { background: var(--accent-tint); color: var(--accent-dark); font-weight: 600; cursor: default; }
.undo-history-empty { padding: 0.5rem 0.6rem; color: var(--muted); font-size: 0.82rem; }

.timer-pill {
  background: var(--danger-tint);
  color: #b91c1c;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.timer-pill.pomodoro { background: var(--warn-tint); color: #c2410c; }
.timer-pill.idle { background: var(--bg); color: var(--muted); border: 1px solid var(--border); }
/* FR#55: the topbar work timer reads as a plain "00:00" clock face until
   started, then blinks red once a second while running — scoped to this one
   pill by ID so the (also .timer-pill) Pomodoro tab pill is unaffected. */
#running-timer-pill:not(.idle) { animation: running-timer-blink 1s step-start infinite; }
@keyframes running-timer-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.4; }
}

.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background: var(--card-bg);
  border-top: 1px solid var(--border);
  padding: 0.4rem 0.2rem calc(0.4rem + var(--safe-bottom));
  z-index: 20;
}
.bottom-nav .nav-btn {
  flex: 1;
  min-width: 0;
  flex-direction: column;
  font-size: 1.2rem;
  gap: 0.1rem;
  padding-left: 0.1rem;
  padding-right: 0.1rem;
  /* FR#335: the whole strip is divided between the five buttons, so there
     is no dead ground between them to miss into. */
  min-height: 44px;
  justify-content: center;
}
.bottom-nav .nav-btn span {
  font-size: 0.6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Left/right collapse toggle and the master to-do panel only exist in the
   desktop sidebar layout — mobile already has its own compact top/bottom
   nav, there's no sidebar there to collapse. */
.panel-collapse-btn { display: none; }
.right-panel { display: none; }

@media (min-width: 860px) {
  /* Nav moves from a horizontal top bar to a persistent left sidebar; the
     same elements just get restyled into a column instead of a row. */
  /* Locks .app to viewport height so a tall .view-root scrolls internally instead of the whole page scrolling the sidebars away. */
  .app { display: flex; flex-direction: column; padding-bottom: 0; height: 100vh; overflow: hidden; }
  .app-body { display: flex; align-items: stretch; flex: 1; min-height: 0; overflow: hidden; }
  .topbar {
    position: relative;
    flex-direction: column;
    align-items: stretch;
    width: 230px;
    flex-shrink: 0;
    /* Stretched by the row rather than measured against the viewport: the
       tab strip above it means the row is no longer the full height. */
    height: auto;
    min-height: 0;
    border-bottom: none;
    border-right: 1px solid var(--border);
    padding: 1.2rem 1rem;
    gap: 0.3rem;
    overflow-y: auto;
    overflow-x: hidden;
    transition: width 0.15s ease;
  }
  /* FR#170/174: an earlier fix let this wrap onto a second line to avoid
     clipping — Sam explicitly doesn't want that (wrapping still crowds the
     nav below it), asking for a smaller font instead, with the icon droppable
     if that's still not enough room. nowrap + a shrunk size handles the
     common case; overflow/ellipsis is a hard backstop against whatever
     system-font-metric differences produced the original report, so at
     worst it truncates instead of ever overflowing or wrapping again.
     The ellipsis has to live on the text span itself, not this flex row —
     text-overflow only fires on the box whose own content overflows, and a
     flex item needs min-width:0 to be allowed to shrink below its content's
     natural width in the first place (same gotcha as FR#161's video caps).
     Icon removal is scoped to the expanded state only — collapsed mode
     already has its own FR#104 rule keeping every nav icon (this one
     included) visible as the entire point of an icon-only sidebar. */
  .topbar-brand { padding: 0 0.5rem 0.8rem; }
  .topbar:not(.collapsed) .topbar-brand .nav-icon { display: none; }
  .topbar-brand span:not(.nav-icon) { font-size: 0.8rem; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  /* margin-top: auto pushes the search box down to sit directly above
     topbar-actions' border-top, instead of floating just under the nav
     items with a large empty gap below it (FR#53). */
  .sidebar-search { display: block; margin-top: auto; }
  .topbar.collapsed .sidebar-search { display: none; }
  .tabbar { display: flex; flex-direction: column; margin-left: 0; gap: 0.2rem; flex: none; }
  .tabbar .nav-btn { justify-content: flex-start; width: 100%; white-space: nowrap; }
  .topbar-actions {
    margin-left: 0;
    flex-wrap: wrap;
    padding-top: 0.8rem;
    border-top: 1px solid var(--border);
  }
  .bottom-nav { display: none; }
  .main-column { flex: 1; min-width: 0; min-height: 0; }
  .view-root { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; }

  /* FR#165: fixed, not absolute — .topbar's overflow-y:auto implicitly
     computes overflow-x to auto too (per spec, whenever overflow-y isn't
     visible), which clips an absolutely-positioned child straddling the
     panel's edge. Fixed positioning is viewport-relative and escapes it. */
  .panel-collapse-btn {
    display: block;
    position: fixed;
    /* Fixed to the viewport, so they sit under the tab strip rather than
       behind it once there is one. */
    top: calc(0.9rem + var(--tab-strip-h, 0px));
    left: calc(230px - 0.9rem);
    right: auto;
    width: 1.8rem;
    height: 1.8rem;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--card-bg);
    color: var(--muted);
    cursor: pointer;
    font-size: 0.65rem;
    line-height: 1;
    z-index: 50;
    box-shadow: var(--shadow);
  }
  /* FR#76: the collapsed (icon-only, 60px) sidebar made this button hard to
     spot — it sits half over the busy boundary with the main content, and
     the default subtle/muted styling blended into both sides. Switch it to
     a solid accent circle specifically while collapsed, so it's obviously
     a button to click rather than a stray line in the layout. */
  .topbar.collapsed .panel-collapse-btn {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    transform: rotate(180deg);
    left: calc(60px - 0.9rem);
  }
  .topbar.collapsed { width: 60px; }
  /* FR#104: these were bare `span` selectors, which also matched the
     .nav-icon/.theme-toggle-icon spans themselves (added later by FR#36) —
     not just the text-label spans they were written to hide — so collapsing
     the sidebar hid every icon along with the labels. Exclude both icon
     classes so only the labels disappear. */
  .topbar.collapsed .topbar-brand span:not(.nav-icon),
  .topbar.collapsed .nav-btn span:not(.nav-icon):not(.theme-toggle-icon),
  .topbar.collapsed .topbar-actions .timer-pill { display: none; }
  .topbar.collapsed .tabbar .nav-btn { justify-content: center; padding-left: 0; padding-right: 0; }
  /* Icon-only sidebar has no room for sub-items anyway — hide the
     expand/collapse chevrons and force every nav-group shut regardless of
     its own expanded state, rather than leaving orphaned icon-only rows. */
  .topbar.collapsed .nav-group-chevron,
  .topbar.collapsed .nav-group-chevron-inline { display: none; }
  .topbar.collapsed .nav-group-body { max-height: 0 !important; }
  .topbar.collapsed .topbar-actions { flex-direction: column; align-items: center; }

  .right-panel {
    display: block;
    position: relative;
    width: 280px;
    flex-shrink: 0;
    border-left: 1px solid var(--border);
    height: auto;
    min-height: 0;
    overflow-y: auto;
    padding: 1.2rem 1rem;
    transition: width 0.15s ease;
  }
  /* FR#133: mirrors .topbar.collapsed — unlike the left nav there's no
     meaningful icon-only version of a to-do list, so collapsed just hides
     everything but the toggle itself and shrinks the panel to nothing.
     Horizontal padding has to be zeroed explicitly too: border-box keeps
     padding inside the declared width, but can't shrink it past its own
     size, so width:0 alone would still leave the padding's space visible.
     No overflow:hidden here (unlike .topbar, which still shows real
     icon-only content that could spill past its shrunk width) — every
     child but the toggle button is already display:none, so there's
     nothing left to clip *except* that same button, which deliberately
     sits outside the box via a negative left offset to stay put as the
     panel shrinks. overflow:hidden clipped it right along with everything
     else, leaving no way to expand the panel again (FR#158). */
  .right-panel.collapsed {
    width: 0;
    padding-left: 0;
    padding-right: 0;
  }
  .right-panel.collapsed > *:not(.right-panel-collapse-btn) { display: none; }
  .right-panel-collapse-btn { left: auto; right: calc(280px - 0.9rem); }
  /* Unlike .topbar (min 60px), .right-panel collapses to 0 width, so its
     edge IS the viewport edge — straddling it like the left button would
     push half the circle past the window. Keep the whole button on-screen. */
  .right-panel.collapsed .right-panel-collapse-btn {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    transform: rotate(180deg);
    right: 0.2rem;
  }
}

.right-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.8rem; }
/* FR#186: extra breathing room above the Deadlines section's own header,
   the one right-panel-header that isn't the sidebar's very first thing.
   FR#266: plus a visible divider line, same border-top+padding-top pairing
   used elsewhere in this file, so the three sidebar sections read as
   distinct blocks rather than one continuous list. */
#master-todo-list + .right-panel-header { margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--border); }
/* FR#215: same spacing for the "Feature requests" header, which now sits
   right after the Deadlines list the same way Deadlines sits after To do's. */
#master-deadlines-list + .right-panel-header { margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.right-panel-header h2 { font-size: 1rem; margin: 0; }
.right-panel-day-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.8rem; font-weight: 600; }
.right-panel-day-nav .icon-btn { padding: 0.1rem 0.4rem; font-size: 0.7rem; }
.master-quick-add-todo { margin-bottom: 0.8rem; display: flex; gap: 0.3rem; }
/* FR#98: a minimum height so there's always real clickable space below a
   short list of to-dos (same "click empty space to add" convention as
   .todo-col, see add-todo-here/focus-master-quickadd), not just a sliver. */
.master-todo-list { min-height: 200px; cursor: pointer; }
.master-quick-add-todo input[type="text"] { flex: 1; min-width: 0; font-size: 0.82rem; padding: 0.4rem 0.6rem; }
.master-quick-add-feature { display: flex; gap: 0.3rem; }
.master-quick-add-feature input[type="text"] { flex: 1; min-width: 0; font-size: 0.82rem; padding: 0.4rem 0.6rem; }
.master-todo-item {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  background: var(--card-bg);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.5rem 0.6rem;
  margin-bottom: 0.35rem;
}
.master-todo-content { flex: 1; min-width: 0; }
/* FR#186: the Deadlines panel reuses .master-todo-item/-content for its
   rows but has no .list-item ancestor for the shared .meta rule to match. */
.master-todo-content .meta { font-size: 0.76rem; color: var(--muted); margin-top: 0.1rem; }
/* FR#234: same whole-row cursor/hover treatment FR#177 gave the main
   Deadlines page's .deadlines-list .list-item rows — data-action already
   sits on the whole .master-todo-item here (calendar-open-item), but only
   .master-todo-title showed a pointer, and there was no hover feedback at
   all. Scoped to this panel specifically, not the to-do panel above it
   (shares .master-todo-item but has its own hover-reveals-checkbox
   behavior via FR#62). */
.master-deadlines-list .master-todo-item { cursor: pointer; }
.master-deadlines-list .master-todo-item:hover { background: var(--bg); }
.master-todo-title { font-size: 0.85rem; font-weight: 600; cursor: pointer; }
/* FR#255: same reset-beating specificity trick as .todo-item .todo-content
   .todo-title-input (see that rule's own comment) — three classes deep so
   the global input reset's border/background/padding doesn't leak back in,
   sized to match .master-todo-title rather than the wider board's version. */
.master-todo-item .master-todo-content .todo-title-input {
  display: block;
  width: 100%;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: inherit;
  background: transparent;
  border: none;
  border-radius: 0;
  margin: -0.2rem -0.3rem;
  padding: 0.2rem 0.3rem;
}
.master-todo-item .master-todo-content .todo-title-input:focus { outline: none; border-color: transparent; box-shadow: none; background: var(--bg); }
/* FR#198: a thin horizontal line at the exact spot the card will land,
   same idea as FR#70's project-card treatment (see that rule's comment)
   but rotated for a vertically-stacked list — drag-before means "above
   this card" here instead of "to its left". */
.master-todo-item.drag-over.drag-before::before,
.master-todo-item.drag-over.drag-after::after {
  content: '';
  position: absolute;
  left: 0.3rem;
  right: 0.3rem;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  z-index: 2;
}
.master-todo-item.drag-over.drag-before::before { top: -0.3rem; }
.master-todo-item.drag-over.drag-after::after { bottom: -0.3rem; }
/* FR#62: the project icon sits at rest where the checkbox goes; hovering
   (or mid-complete via .completing) swaps the two so the row identifies its
   project by icon alone, but still offers a real checkbox to act on. */
.master-todo-check { flex: none; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; padding-top: 0.1rem; cursor: pointer; }
.master-todo-check input { display: none; width: 16px; height: 16px; cursor: pointer; }
.master-todo-item:hover .master-todo-check .project-icon,
.master-todo-item.completing .master-todo-check .project-icon { display: none; }
.master-todo-item:hover .master-todo-check input,
.master-todo-item.completing .master-todo-check input { display: block; }
.master-todo-item.completing .master-todo-title { text-decoration: line-through; opacity: 0.55; }
/* FR#74: sunk to the bottom of today's list, crossed off, but otherwise a
   normal row — icon at rest / checkbox on hover like any other item, unlike
   the momentary .completing state right after clicking which forces the
   checkbox to stay visible. Stays this way until midnight rolls the panel
   onto a new date, at which point it stops matching and simply isn't shown. */
.master-todo-item.done-today .master-todo-title { text-decoration: line-through; opacity: 0.55; }
/* Asked for: a checked-off meeting stays in the Agenda list, crossed out.
   A meeting that happened is still part of that day. */
.master-todo-item.done .master-todo-title { text-decoration: line-through; opacity: 0.55; }
.right-panel.drag-over { background: var(--accent-tint); }

.view-root {
  padding: 1.2rem 1.5rem;
}

/* ---------- Cards / generic ---------- */
.card {
  background: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1rem 1.2rem;
  margin-bottom: 1rem;
}
.shortcut-card { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.shortcut-card .actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
.meeting-notes-text { line-height: 1.7; }
.meeting-note-heading { font-size: 0.82rem; font-weight: 600; color: var(--muted); margin-bottom: 0.2rem; }
/* Filmplanner-sectie: kop met de openen-knop, en de dagtabel eronder. */
.filmplanner-head { display: flex; gap: 1rem; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-bottom: 0.8rem; }
.filmplanner-head .form-actions { margin-top: 0; align-items: center; }
.filmplanner-table { overflow-x: auto; }
.filmplanner-days { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.filmplanner-days th { text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); padding: 0.35rem 0.5rem 0.35rem 0; }
.filmplanner-days td { border-top: 1px solid var(--border); padding: 0.4rem 0.5rem 0.4rem 0; }
.filmplanner-days td:first-child { font-variant-numeric: tabular-nums; color: var(--muted); width: 3rem; }

.shortcut-open-link { font-size: 1.05rem; font-weight: 600; color: var(--accent); text-decoration: none; word-break: break-all; }
.shortcut-open-link:hover { text-decoration: underline; }
/* FR#225: flex-start (not center) — with center, a row with no action
   button/select next to the heading is only as tall as the h2 itself, but
   one with a button/select (taller, from its own padding) stretches the
   row and re-centers the (shorter) h2 inside the extra height, so its text
   actually rendered a few pixels lower on pages with page-header actions
   than on pages without any. flex-start pins every h2 to the row's own
   top edge regardless of what else shares the row, so its position no
   longer depends on which page-specific actions happen to sit next to it. */
.section-title {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin: 1.4rem 0 0.6rem;
}
.section-title h2 { font-size: 1.05rem; margin: 0; }
.section-title:first-child { margin-top: 0; }
/* FR#286: a visible divider between Home's sections (quick capture /
   recent projects / recent notes / recent activity, plus phone's own
   to-do's/calendar panels — see viewHome) — the ~1.4rem gap above was
   whitespace only, easy to misread as one long section rather than
   several distinct ones. The general-sibling selector reaches every
   .section-title except whichever is first among its own siblings
   (Recent projects, immediately under the quick-capture card; To do's,
   first inside .home-mobile-panels), without needing a first-child
   check that .section-title not literally being the first DOM child
   would defeat. */
.home-page .section-title ~ .section-title { border-top: 1px solid var(--border); padding-top: 1rem; }
/* FR#391: opted out per section. The dividers separate Home's own sections
   from each other; above the Inbox one it just drew a line under the page. */
.home-page .section-title.no-divider { border-top: none; padding-top: 0; }
/* FR#142: a second action next to an existing one within .section-title's
   own single flex-item slot (see also .notes-page-header-actions, the
   same idea scoped to the Notes page specifically). */
.section-title-actions { display: flex; align-items: center; gap: 0.5rem; }
/* Gevraagd: het agenda-icoontje op deze knop stond te hoog ten opzichte van
   de tekst. Het icoon is een blokje op zichzelf; zonder uitlijning gaat het
   op de onderkant van de regel staan in plaats van op het midden ervan. */
.section-title-actions button { display: inline-flex; align-items: center; gap: 0.35rem; }
.section-title-actions button .nav-icon { align-items: center; }
.section-title-actions button .nav-icon svg { width: 1em; height: 1em; }
/* FR#127: Notes page header needs two actions (Recently deleted + New note)
   side by side as .section-title's own single flex item. FR#176: align-self
   (not .section-title's shared align-items, which every other page's own
   section header also relies on) keeps this top-aligned with the "Notes"
   heading instead of centered against its taller line-height, without
   touching how any other page's section-title lays out. */
.notes-page-header-actions { display: flex; align-items: center; gap: 0.5rem; align-self: flex-start; }
/* FR#274: the project-picker only makes sense while actively editing —
   otherwise it's just noise while browsing/reading the notes overview.
   Hidden by default, revealed by the .note-editing class toggled on
   focus/blur of the note's own editable (see the focusin/focusout
   listeners in app.js) — scoped to this page-level header specifically,
   not the per-project Notes tab's own embedded copy of these controls. */

.empty-state { color: var(--muted); text-align: center; padding: 1.5rem 0; font-size: 0.9rem; }

button.small, label.small {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.4rem 0.8rem;
  font-size: 0.85rem;
  cursor: pointer;
  display: inline-block;
}
button.small:hover, label.small:hover { filter: brightness(0.94); }
/* FR#73: two separate reposition boxes, one per crop shape. The header-
   banner box's aspect-ratio is overwritten inline by mountCoverRepositionBoxes()
   to match the real header exactly; this 3/1 is just the fallback before
   that measurement runs. The square box needs no such measurement. */
.cover-reposition-boxes { display: flex; flex-wrap: wrap; gap: 1.2rem; margin-bottom: 0.6rem; }
.cover-reposition-box { flex: 1 1 240px; min-width: 160px; }
.cover-upload-preview { display: block; width: 100%; max-width: 320px; aspect-ratio: 3 / 1; object-fit: cover; border-radius: 8px; cursor: grab; touch-action: none; }
.cover-upload-preview-square { max-width: 140px; aspect-ratio: 1 / 1; }
.cover-upload-preview:active { cursor: grabbing; }
button.small.secondary { background: var(--accent-tint); color: var(--accent-dark); }
button.small.secondary.active { background: var(--accent); color: #fff; }
button.small.danger { background: var(--danger-tint); color: var(--danger); }
button.ghost {
  background: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.4rem 0.7rem;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--text);
}
button.ghost:hover { background: var(--bg); }
button.tiny {
  background: none;
  border: none;
  border-radius: 6px;
  padding: 0.2rem 0.35rem;
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
  color: var(--muted);
  flex-shrink: 0;
}
button.tiny:hover { background: var(--bg); }
button.tiny.danger { color: var(--danger); }
/* FR#259: the bin icon reads as ordinary text color, not red — it shows up
   on nearly every row/card across the app, and staying red everywhere read
   as more alarming than intended for something clicked this often. Kept as
   a separate modifier rather than changed on .small.danger/.tiny.danger
   directly, since those two are also "Stop timer" and "Delete forever" —
   text buttons where the red *is* the message and stays as-is. */
button.small.danger-icon, button.tiny.danger-icon { background: none; color: var(--text); }
button.small.danger-icon:hover, button.tiny.danger-icon:hover { background: var(--bg); }

/* ---------- Project cards ---------- */
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.8rem;
}
/* Home's "Recent projects" (FR#90): shrinks down to a minimum instead of
   the Projects page's 220px, so every recent project fits on screen
   without scrolling — replaces FR#60's single-row horizontal-scroll
   carousel. Still just .project-grid underneath (mountProjectDragHandlers
   selects on that class for drag-reorder, which still applies here). */
/* FR#122: auto-fit (not auto-fill) — auto-fill reserves a full row's worth
   of tracks even when there are only a couple of projects, leaving them
   stuck at the minimum with a wide empty gap alongside; auto-fit collapses
   the unused tracks so the 1fr sizing actually grows the real cards to
   fill the row instead. */
/* FR#199: same 150px floor as .notes-carousel (was 100px) — with viewHome
   now also capping both sections to the same item count, matching this
   minimum too is what actually makes the resulting cards the same size,
   since both grids share the same container width. */
/* FR#122 originally chose auto-fit here so a couple of projects would grow
   to fill the row rather than sit small beside a gap. With few enough
   items that backfires badly: one project stretches to the full row width,
   and since these cards are square (aspect-ratio: 1/1) a full-width card
   is also a full-width-TALL card, so an account with one project got a
   Home page dominated by a single enormous tile. auto-fill keeps the
   tracks a fixed size, so cards look identical whether you have one
   project or twelve — the row just has empty space in it, which is the
   trade Sam asked for. */
.home-recent-projects {
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
/* One row, always. This is a glance at what you've touched recently, not
   the full list — "Alle projecten" is right there for that — so it should
   never grow a second row and push the rest of Home down. Anything past
   the first row lands in an implicit row, and implicit rows are collapsed
   to nothing and clipped. Desktop only: below 860px .project-grid becomes
   a vertical list (see the mobile block), where rows are the point and a
   separate nth-child cap already limits the count. */
@media (min-width: 860px) {
  .home-recent-projects {
    /* The first row has to be explicit, or it counts as implicit too and
       grid-auto-rows collapses the whole grid to nothing. */
    grid-template-rows: auto;
    grid-auto-rows: 0;
    row-gap: 0;
    overflow: hidden;
  }
}
/* FR#97: the 1rem title (sized for the Projects page's 220px cards) read as
   oversized once FR#90 shrank these down to a 100px minimum. */
.home-recent-projects .project-card-content { padding: 0.5rem; }
.home-recent-projects .project-card-content h3 { font-size: 0.78rem; }

/* FR#281: phone-only mirror of the desktop right sidebar's to-do/calendar
   panels, reusing its exact classes (.right-panel-day-nav/.master-todo-list/
   .master-deadlines-list/.master-quick-add-todo-mount) so renderMasterTodoPanel
   and renderMasterDeadlinesPanel populate both copies from one fetch —
   see viewHome(). The sidebar itself already covers this on desktop
   (.right-panel's own min-width:860px media query), so this block only
   ever needs hiding there, never showing. */
.home-mobile-panels { margin-top: 1.4rem; }
@media (min-width: 860px) {
  .home-mobile-panels { display: none; }
}

/* ---------- Home: recent activity (FR#121) ---------- */
.home-activity-row.inbox-hidden { display: none; }
.inbox-more-btn { width: 100%; margin-top: 0.4rem; }
.home-activity-feed { display: flex; flex-direction: column; gap: 0.3rem; }
.home-activity-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border-radius: 8px;
  cursor: pointer;
}
.home-activity-row:hover { background: var(--bg); }
.home-activity-row .project-icon { flex-shrink: 0; border-radius: 6px; }
.home-activity-text { min-width: 0; font-size: 0.85rem; }
.home-activity-text .meta { font-size: 0.75rem; }
/* FR#178: shrink-to-fit grid instead of a fixed-200px horizontal-scroll
   strip — same auto-fit/1fr technique as .home-recent-projects, so cards
   shrink to fit the row instead of needing a scrollbar. The old flex-row
   version fought a persistent (if narrow) vertical-scrollbar leak from the
   CSS Overflow spec auto-promoting overflow-y whenever overflow-x wasn't
   visible; this has no scroll axis at all, so that whole class of bug is
   moot rather than fixed. */
/* auto-fill, not auto-fit — same square-card reasoning as .home-recent-projects above. */
.notes-carousel { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.9rem; }
/* One row, like Recente projecten above it: this is a glance at what you
   were last writing, not the note list, and a second row would push the
   rest of Home down. Extra cards land in implicit rows, which collapse and
   clip. The first row must be declared explicitly or it counts as implicit
   too and grid-auto-rows: 0 flattens the whole section. Desktop only —
   below 860px this becomes a vertical list where rows are the point. */
@media (min-width: 860px) {
  .notes-carousel {
    grid-template-rows: auto;
    grid-auto-rows: 0;
    row-gap: 0;
    overflow: hidden;
  }
}
.note-carousel-item { min-width: 0; }
/* FR#162: square regardless of content — width now comes from the grid
   track above, so aspect-ratio just matches height to it. */
.note-carousel-item .note-grid-card { margin-bottom: 0.5rem; aspect-ratio: 1 / 1; }
.note-carousel-item .idea-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* FR#179: 3 lines (FR#162's original choice) left a lot of visible empty
   space below the text on most real notes. .idea-snippet-text (the title+
   preview's shared container) always fills the card's full ~128px content
   height via flex:1 regardless of how much text is actually in it — the
   line-clamp value is the only thing controlling how much of that space
   the preview text itself uses. 6 lines (~96px) plus the always-single-
   line title (~18px, forced by white-space:nowrap above) fits with a
   little headroom to spare; .idea-snippet-text's own overflow:hidden is
   still the backstop, so a genuinely short note just shows fewer lines
   rather than this ever overflowing the card. */
.note-carousel-item .idea-preview-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  overflow: hidden;
}
.note-carousel-caption { cursor: pointer; }
.note-carousel-project { display: flex; align-items: center; gap: 0.3rem; font-weight: 600; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.note-carousel-date { font-size: 0.78rem; color: var(--muted); }
.note-grid-card {
  background: var(--card-bg);
  /* FR#285: in light mode specifically, .view-root's background is also
     --card-bg (FR#144's sidebar/pane swap made the content pane white),
     so these cards previously had nothing but --shadow's faint 6%-opacity
     edge to read as distinct cards against the page — easy to lose in a
     tight grid of several small ones. --border is already tuned to show
     up against both --bg and --card-bg in either theme. */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.9rem 1rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
}
.note-grid-card:hover { background: var(--bg); }
/* FR#68: was targeting .idea-snippet, but this card's content is a bare
   .idea-snippet-text (no wrapper div) — the rule never matched anything,
   so the title/preview block just sat at its natural height instead of
   growing to push the meta line to the card's bottom edge. The divider
   look (padding/margin/border under the title) is dropped too — it was
   left over from the old standalone Notes grid page (now the master-detail
   layout, FR#39); in this compact carousel card it just reads as an
   unwanted gap shoving the preview text down away from its title. */
.note-grid-card .idea-snippet-text { flex: 1; min-height: 0; overflow: hidden; }
.note-grid-card .meta { margin-top: 0.4rem; }
.project-card {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  cursor: pointer;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  background-size: cover;
  background-position: center;
}
.project-card:hover { opacity: 0.88; }
.project-card-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.25) 55%, rgba(0, 0, 0, 0) 100%);
}
.project-card-content { position: relative; z-index: 1; padding: 0.9rem; width: 100%; color: #fff; }
.project-card-content h3 { margin: 0; font-size: 1rem; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); }
.project-label-badge {
  display: inline-block;
  margin-top: 0.3rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.25);
  backdrop-filter: blur(2px);
}
.project-favorite-star {
  position: absolute;
  z-index: 2;
  top: 0.5rem;
  right: 0.5rem;
  border: none;
  background: rgba(0, 0, 0, 0.35);
  color: rgba(255, 255, 255, 0.85);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}
.project-favorite-star.active { color: #fbbf24; }
.project-favorite-star:hover { background: rgba(0, 0, 0, 0.55); }
/* FR#67: an Apple-Mail-style unread-count badge for to-dos due today on
   this project — opposite corner from the favorite star, only rendered
   when there's a nonzero count (an app icon badge doesn't show "0"). */
.project-today-badge {
  position: absolute;
  z-index: 2;
  top: 0.5rem;
  left: 0.5rem;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  border-radius: 999px;
  background: #ef4444;
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 22px;
  text-align: center;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* FR#207: FR#185 originally scoped --accent/--accent-dark/--accent-tint to
   .project-detail-root here, so every button/tab/chip/focus ring anywhere in
   a project's page picked up that project's derived hue instead of the
   app's own house color — buttons is what actually read as "wrong" about
   that. The --p-hue/--p-sat/--p-tint-sat custom properties are still set
   inline on this wrapper (projectAccentStyle) and project.color is still
   computed and stored (syncProjectAccentColor) — the header/cover banner
   itself already gets its color straight from the chosen photo/gradient,
   not through --accent, so removing this block only stops the color from
   leaking into generic UI chrome. It doesn't undo choosing a color.
*/

/* ---------- Project detail tabs ---------- */
.detail-header {
  position: relative;
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
}
.detail-header.has-cover {
  background-size: cover;
  background-position: center;
  padding: 1.6rem 1.2rem;
}
.detail-header-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to right, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.15));
  border-radius: var(--radius);
}
.detail-header-content { position: relative; z-index: 1; display: flex; align-items: center; gap: 0.8rem; }
.detail-header.has-cover .detail-header-content { color: #fff; }
.detail-header h1 { margin: 0; font-size: 1.3rem; }

/* ---------- Project icon: cropped cover image, not emoji (FR#57) ---------- */
.project-icon {
  display: inline-block;
  flex-shrink: 0;
  border-radius: 8px;
  background-size: cover;
  background-position: center;
  vertical-align: middle;
}

/* ---------- Project header click-to-edit (FR#52) ---------- */
.detail-header-icon-btn {
  background: none;
  border: none;
  padding: 0.2rem;
  border-radius: 10px;
  cursor: pointer;
  line-height: 0;
}
.detail-header:not(.has-cover) .detail-header-icon-btn:hover { background: var(--bg); }
.detail-header.has-cover .detail-header-icon-btn:hover { background: rgba(255, 255, 255, 0.15); }
/* FR#241: <button> doesn't inherit color from its parent by default (a
   plain UA-stylesheet quirk, unlike the h1/code-chip text right next to
   it) — so the settings hamburger stayed the browser's default dark color
   even though .has-cover already turns the rest of the header white,
   nearly invisible against a dark cover. Hover background needs its own
   override too, matching .detail-header-icon-btn above — the plain
   var(--bg) hover is light gray, which white text would equally vanish
   into. */
.detail-header.has-cover .icon-btn { color: #fff; }
.detail-header.has-cover .icon-btn:hover { background: rgba(255, 255, 255, 0.15); }
.detail-header h1[data-action] { cursor: pointer; border-radius: 6px; padding: 0.1rem 0.4rem; margin: 0 0 0 -0.4rem; display: inline-block; }
.detail-header:not(.has-cover) h1[data-action]:hover { background: var(--bg); }
.detail-header.has-cover h1[data-action]:hover { background: rgba(255, 255, 255, 0.15); }
/* Same input-reset specificity trap as .todo-title-input (see the comment
   near that rule) — a bare .project-title-input class loses its font-size/
   padding/background back to the global input:not([type=checkbox]):not(
   [type=radio]) reset. Three classes deep beats it without !important. */
.detail-header .detail-header-content .project-title-input {
  font-size: 1.3rem;
  font-weight: 700;
  font-family: inherit;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.15rem 0.5rem;
  width: 100%;
  max-width: 420px;
  background: var(--card-bg);
  color: var(--text);
}
.project-code-row { margin-top: 0.3rem; }
.project-code-chip {
  background: var(--accent-tint);
  color: var(--accent-dark);
  border: none;
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-size: 0.75rem;
  cursor: pointer;
}
.project-code-chip.muted { background: none; border: 1px dashed var(--border); color: var(--muted); }
.detail-header.has-cover .project-code-chip { background: rgba(255, 255, 255, 0.2); color: #fff; }
.detail-header.has-cover .project-code-chip.muted { border-color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.85); }
/* FR#214: same input-reset specificity trap as .project-title-input right
   above — a bare .project-code-input class lost its padding/font-size back
   to the global reset, which made the input ~35px tall against the chip's
   ~19px and visibly shoved .subtabs (and everything below it) down for as
   long as the code was being edited. Three classes deep beats it. */
.detail-header .detail-header-content .project-code-input {
  font-size: 0.75rem;
  font-family: inherit;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  width: 130px;
  background: var(--card-bg);
  color: var(--text);
  text-transform: uppercase;
}
.cover-edit-panel { margin-top: 0.8rem; }
.cover-edit-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.7rem; }
.cover-edit-panel-title { font-weight: 600; font-size: 0.85rem; color: var(--text); }
.project-timestamps { font-size: 0.78rem; margin-bottom: 1rem; }
.gradient-swatches { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.6rem; }
.gradient-swatch {
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 8px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}
.gradient-swatch.selected { border-color: var(--accent); }
.gradient-swatch.none { background: var(--neutral-tint); color: var(--muted); font-size: 1rem; }
.cover-source-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem; margin-bottom: 0.6rem; }
.stock-photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.stock-photo-swatch {
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  overflow: hidden;
  line-height: 0;
  background: none;
}
.stock-photo-swatch:hover { border-color: var(--accent); }
.stock-photo-swatch img { width: 100%; height: 60px; object-fit: cover; display: block; }
.stock-photo-footer { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; }
.subtabs {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  overflow-x: auto;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1rem;
  padding-bottom: 2px;
}
.subtab-row { display: flex; align-items: center; flex-shrink: 0; }
.subtabs button {
  background: none;
  border: none;
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  font-size: 0.88rem;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.subtabs button svg { width: 1.05em; height: 1.05em; flex-shrink: 0; }
.subtab-row button:not(.subtab-remove):not(.active):hover { color: var(--text); background: var(--bg); }
.subtab-row button.active { color: var(--accent-dark); border-color: var(--accent); font-weight: 600; }
.subtab-remove {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--muted);
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
  flex-shrink: 0;
}
.subtab-remove:hover { color: var(--danger); }
.subtab-add {
  background: none;
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 0.4rem 0.7rem;
  font-size: 0.82rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.subtab-add:hover { border-color: var(--accent); color: var(--text); }
.add-section-panel { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.6rem 0 1rem; width: 100%; }
.chip {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.35rem 0.8rem;
  font-size: 0.82rem;
  cursor: pointer;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.chip:hover { background: var(--accent-tint); border-color: var(--accent); }
.chip svg { width: 1em; height: 1em; flex-shrink: 0; }
.section-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-top: 1rem; }

.view-toggle { display: flex; gap: 0.4rem; margin-bottom: 0.9rem; }
/* FR#220: matches .time-manager-filters select's own explicit height below
   — same padding/font-size/border on both already, but a native <select>
   still renders a couple pixels taller than a <button> given the exact same
   box, so height has to be pinned explicitly for the two to actually align. */
.view-toggle button {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  height: 2rem;
  font-size: 0.85rem;
  cursor: pointer;
  color: var(--muted);
}
.view-toggle button.active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }

/* ---------- Visual timeline ---------- */
/* FR#93: height/overflow-y removed — mountTimelineZoomFit() now fits the
   default zoom to the container's actual width, and letting canvasHeight
   size this box naturally (instead of clipping it to a fixed 60vh with its
   own inner scrollbar) is what makes all rows "vertically fully visible";
   the page itself scrolls for a project with enough items to need it,
   same as everywhere else in the app. overflow-x stays — a manual zoom-in
   past fit-width, or a span too long to fit even at the minimum zoom, still
   needs somewhere to scroll horizontally. */
.timeline-visual { width: 100%; }
.timeline-scroll {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  padding: 0.5rem 0;
}
.timeline-zoom-controls { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.6rem; }
.timeline-zoom-label { font-size: 0.8rem; color: var(--muted); min-width: 3em; text-align: center; }
.timeline-canvas { position: relative; }
.timeline-grid { position: absolute; top: 0; left: 0; right: 0; bottom: 0; }
.timeline-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); }
.timeline-tick span { position: absolute; top: 2px; left: 5px; font-size: 0.7rem; color: var(--muted); white-space: nowrap; }
.timeline-today { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--accent); }
.timeline-rows { position: absolute; left: 0; right: 0; }
.timeline-row { position: absolute; left: 0; right: 0; height: 36px; }
.timeline-bar {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  height: 14px;
  background: var(--accent);
  cursor: pointer;
}
.timeline-bar.range { border-radius: 4px; cursor: grab; }
.timeline-bar.range:active, .timeline-bar.range.dragging { cursor: grabbing; }
.timeline-bar.point { border-radius: 50%; cursor: grab; }
.timeline-bar.point:active, .timeline-bar.point.dragging { cursor: grabbing; }
.timeline-bar.selected, .timeline-bar:hover { background: var(--accent-dark); }
/* FR#93: a narrow grab strip at each end of a range bar, for resizing its
   start/end independently of dragging the bar itself to move both dates
   together — widened well past its own 6px so it's actually easy to grab
   without needing pixel-perfect aim on a thin 14px-tall bar. */
.timeline-bar-handle { position: absolute; top: -4px; bottom: -4px; width: 6px; cursor: ew-resize; }
.timeline-bar-handle.start { left: -3px; }
.timeline-bar-handle.end { right: -3px; }
/* FR#237: floated into view via a small JS transform (mountTimelineLabelFloat
   in app.js) while any part of its bar is on screen during a horizontal
   scroll, so a zoomed-in long bar's name stays readable no matter which
   part of the bar is currently visible. position:sticky was tried first
   but doesn't engage here — it only takes effect when a label sits at
   offset 0 within its containing block, and this one naturally sits offset
   by the bar's own width — so this is done by hand instead. The
   background/padding keep it legible over whatever grid tick or the
   today-line it ends up floating on top of. */
.timeline-label {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.8rem;
  white-space: nowrap;
  cursor: pointer;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  background: var(--card-bg);
  padding: 0 3px;
  border-radius: 3px;
}
.timeline-label.selected { font-weight: 600; color: var(--accent-dark); }
.timeline-detail { margin-top: 0.8rem; }
.timeline-detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4rem; }

@media (min-width: 860px) {
  .project-layout { display: flex; gap: 1.6rem; align-items: flex-start; }
  .subtabs {
    flex-direction: column;
    align-items: stretch;
    border-bottom: none;
    border-right: 1px solid var(--border);
    padding-right: 1rem;
    padding-bottom: 0;
    margin-bottom: 0;
    min-width: 170px;
    overflow-x: visible;
  }
  .subtab-row { justify-content: space-between; }
  .subtabs button { text-align: left; border-bottom: none; border-left: 2px solid transparent; border-radius: 0 8px 8px 0; }
  .subtab-row button.active { background: var(--accent-tint); border-color: var(--accent); }
  .tab-body { flex: 1; min-width: 0; }
  .add-section-panel { flex-direction: column; }
}

/* ---------- Todos ---------- */
/* FR#408: one column at every width — today, the rest of the week, then
   next week and later, read straight down. */
.todo-columns { display: block; }
.todo-col { cursor: pointer; border-radius: 10px; padding: 0.3rem; margin: -0.3rem; }
.todo-col h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); margin-bottom: 0.5rem; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.future-filter-select {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  color: var(--text);
  font-size: 0.75rem;
  padding: 0.15rem 0.35rem;
  border-radius: 6px;
  cursor: pointer;
}
/* FR#91: an always-visible "+" in the column header — clicking empty space
   under a column already quick-adds (FR#66), but that space isn't visible
   without scrolling once a column has enough cards to fill it. */
.todo-col-header-actions { display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; }
.todo-col-add {
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.todo-col-add:hover { background: var(--border); color: var(--text); }
.todo-item {
  position: relative;
  background: transparent;
  border-radius: 8px;
  /* Tightened once the arrows and the urgency stripe left: a to-do is one
     line of text, and the board shows a whole week of them at once. */
  padding: 0.35rem 0.5rem;
  margin-bottom: 0.1rem;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  cursor: default;
  /* pan-y, not none. The whole row used to be the drag handle, so it had
     to swallow every gesture — but the handle is the grip now, and `none`
     here meant a finger landing on a to-do could not scroll the page at
     all. Which, on a board that is mostly to-dos, is most of the page.
     pan-y hands vertical scrolling back to the browser and leaves
     horizontal to the swipe-to-delete handler, which is the only other
     gesture the row itself answers. */
  touch-action: pan-y;
  transition: background 120ms ease, box-shadow 120ms ease;
}
/* Asked for: a grip to the left of a to-do, showing itself on hover so the
   row stays quiet until you reach for it. It is also what the drag starts
   from, together with the checkbox area beside it. */
.todo-drag-grip {
  flex: none;
  width: 1.1rem;
  padding: 0.3rem 0;
  margin: -0.3rem 0 -0.3rem -0.25rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1;
  cursor: grab;
  touch-action: none;
  user-select: none;
  /* Asked for: invisible until the card is actually moving, everywhere.
     A mark in front of every row is a lot of furniture for something you
     reach for rarely — and the drag starts from the checkbox area beside
     it regardless, so nothing is lost by not drawing it. It keeps its
     width, so nothing shifts sideways the moment a drag begins. */
  opacity: 0;
  transition: opacity 120ms ease;
}
.todo-drag-grip:active { cursor: grabbing; }
.todo-item.dragging .todo-drag-grip,
.todo-appointment.dragging .todo-drag-grip { opacity: 0.85; }
.todo-check-area {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.2rem;
  margin: -0.15rem 0;
  cursor: grab;
  flex: none;
  touch-action: none;
}
.todo-check-area:active { cursor: grabbing; }
/* De rij pak je op, de woorden typ je in — en de cursor zegt welke van de
   twee je onder je muis hebt. */
.todo-day-body-inner .todo-item { cursor: grab; }
.todo-day-body-inner .todo-item.dragging { cursor: grabbing; }

/* Asked for: geen rand of schaduw om een to-do. Wat overblijft is de
   markering: klik je een rij aan zonder te slepen, dan krijgt hij een
   achtergrondkleur, zodat je ziet welke je vasthebt. */
.todo-item.is-selected,
.todo-appointment.is-selected { background: var(--accent-tint); }

/* Dragging one still needs it to read as a picked-up object, and there a
   fill is the point: it has to cover whatever it passes over. */
.todo-item.dragging { background: var(--card-bg); box-shadow: var(--shadow); }
/* Editing the words is a different mode, and the fill says so. */
.todo-item:has(.todo-title-input) { background: var(--card-bg); }
.todo-item.done { opacity: 0.6; text-decoration: line-through; }
.todo-content { flex: 1; min-width: 0; }
/* FR#166: only the title itself is a click-to-edit target now (the rest of
   the card — urgency border, waiting-on/due lines, delete button — used to
   get replaced wholesale by a full form, which is what caused the reported
   "shifts the position of the text" jump). The negative-margin/padding pair
   enlarges the hover/click hit area without taking any extra space in the
   card's own layout, same trick .todo-content used before. */
/* inline-block met max-width: de woorden zijn zo breed als ze zijn, niet
   zo breed als de rij. Anders was de hele rij "tekst", en kon je nergens
   naast de woorden klikken om de to-do te markeren of op te pakken. Lange
   to-do's lopen nog gewoon af op de volle breedte. */
.todo-item .todo-title { display: inline-block; max-width: 100%; overflow-wrap: anywhere; font-size: 0.92rem; cursor: text; border-radius: 6px; margin: -0.2rem -0.3rem; padding: 0.2rem 0.3rem; }
/* Every text input gets a border/padding/background/font-size reset near the
   top of this file (input:not([type="checkbox"]):not([type="radio"])) whose
   two :not() clauses out-specificity a plain class selector — a single
   .todo-title-input rule would silently lose its padding override back to
   that reset (breaking the exact "same box as the div" alignment this is
   built on) and keep the reset's visible border/background too. Three
   classes deep beats it outright without needing !important. */
.todo-item .todo-content .todo-title-input {
  display: block;
  width: 100%;
  font: inherit;
  font-size: 0.92rem;
  color: inherit;
  background: transparent;
  border: none;
  /* Asked for: square, not rounded. The radius only ever showed on the
     focused field's own background, which made the one row being edited
     look like a different shape from every row around it. */
  border-radius: 0;
  margin: -0.2rem -0.3rem;
  padding: 0.2rem 0.3rem;
  /* A textarea, not a single-line input: a to-do long enough to wrap was
     being squeezed onto one scrolling line the moment you edited it, so
     the words moved under you. It grows to its content (growTodoTitleInput)
     and wraps exactly where the plain text did. */
  resize: none;
  overflow: hidden;
  line-height: inherit;
  min-height: 0;
}
.todo-item .todo-content .todo-title-input:focus { outline: none; border-color: transparent; box-shadow: none; background: var(--bg); }
.todo-item .meta { font-size: 0.75rem; color: var(--muted); margin-top: 0.2rem; }
.todo-item .waiting-on { font-size: 0.78rem; color: var(--warn); margin-top: 0.3rem; }
.todo-item .due { font-size: 0.75rem; color: var(--muted); margin-top: 0.2rem; }
/* FR#211: "Set date"/"Waiting on" from a to-do's right-click menu — a single
   floating input dropped next to the item, not tied to any card template. */
.todo-inline-picker {
  width: 200px;
  padding: 0.4rem 0.6rem;
  font-size: 0.85rem;
  background: var(--card-bg);
  border: 1px solid var(--accent);
  border-radius: 6px;
  box-shadow: var(--shadow);
  z-index: 750;
}
.todo-col.drag-over { background: var(--accent-tint); outline: 2px dashed var(--accent); outline-offset: 4px; }
/* A day is a drop target in its own right — for one card, and for a whole
   project's work moved there by its heading. Understated, because the
   insertion bar already says where the thing will land; the outline is
   only for a drop onto empty space, where there is no bar to draw. */
/* Gevraagd: tijdens het slepen alleen het streepje laten zien waar hij
   landt, niet de hele dag oplichten. Het streepje zegt het al precies; de
   gekleurde vlakte eromheen zegt hetzelfde nog eens, maar vager.

   Twee uitzonderingen, allebei omdat er dan géén streepje te tekenen is:
   een ingeklapte dag (daar staat geen rij om het bij te zetten) en een dag
   die nog leeg is. Dan is de vlakte het enige antwoord op "waar komt hij
   terecht". */
.todo-day.drag-over { background: none; }
/* Gevraagd: sleep je een to-do naar een dag waar dat project nog niet staat,
   dan mag je op de dag zelf loslaten — en zie je vooraf een lijntje onderaan
   die dag, zodat duidelijk is waar hij terechtkomt. */
.todo-day.drag-over:not(.is-collapsed) .todo-day-body-inner::after {
  content: '';
  display: block;
  height: 3px;
  margin: 0.45rem 0 0.15rem;
  border-radius: 2px;
  background: var(--accent);
}
.todo-day.is-collapsed.drag-over,
.todo-day.drag-over:not(:has(.todo-item)) { background: var(--accent-tint); border-radius: 8px; }

/* Asked for: the dashed box means "this drop changes which project it
   belongs to", nothing else. Dragging within a project used to light the
   whole group up, which read as a warning when it was just a reorder —
   :has() lets the group holding the dragged item opt out of its own
   highlight. */
.todo-project-group.drag-over {
  outline: 2px dashed var(--accent);
  outline-offset: 4px;
  border-radius: 8px;
}
.todo-project-group.drag-over:has(.dragging) {
  outline: none;
  background: none;
}
.todo-quick-add { background: var(--card-bg); border-radius: 10px; box-shadow: var(--shadow); padding: 0.6rem 0.7rem; margin-bottom: 0.5rem; cursor: default; }
.todo-quick-add input[type="text"] { width: 100%; margin-bottom: 0.5rem; }

/* ---------- Feature requests ---------- */
.feature-columns {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}
@media (min-width: 700px) {
  .feature-columns { grid-template-columns: repeat(3, 1fr); }
}
.feature-col { border-radius: 10px; padding: 0.3rem; margin: -0.3rem; }
.feature-col h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); margin-bottom: 0.5rem; }
.feature-col.drag-over { background: var(--accent-tint); outline: 2px dashed var(--accent); outline-offset: 4px; }
.feature-item {
  background: var(--card-bg);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.7rem 0.8rem;
  margin-bottom: 0.5rem;
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  cursor: grab;
  touch-action: none;
}
.feature-item.dragging { opacity: 0.5; }
.feature-content { flex: 1; min-width: 0; cursor: pointer; border-radius: 6px; margin: -0.2rem -0.3rem; padding: 0.2rem 0.3rem; }
.feature-content:hover { background: var(--bg); }
.feature-title { font-size: 0.92rem; }
.feature-notes { font-size: 0.8rem; margin-top: 0.3rem; }
.feature-add-form { display: flex; gap: 0.5rem; }
.feature-add-form input[type="text"] { flex: 1; }
.todo-quick-add .form-actions { margin: 0; }

/* ---------- Drag and drop (ideas + to-dos) ---------- */
.drag-handle { cursor: grab; touch-action: none; }
.dragging { opacity: 0.55; box-shadow: 0 8px 24px rgba(20, 20, 40, 0.18); position: relative; z-index: 1000; cursor: grabbing; }
.idea-item.drag-over { outline: 2px solid var(--accent); outline-offset: 2px; }
.sidebar-favorite-btn.drag-over { outline: 2px solid var(--accent); outline-offset: 2px; }
/* FR#198: same rotated line treatment as .master-todo-item above, for the
   to-do board's own cards (vertically stacked within a column).
   The line sits just inside the card rather than in the gap above/below
   it: swipe-to-delete gave .todo-item overflow:hidden, which would clip
   anything drawn outside the box. */
.todo-item.drag-over.drag-before::before,
.todo-item.drag-over.drag-after::after {
  content: '';
  position: absolute;
  left: 0.3rem;
  right: 0.3rem;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  z-index: 2;
}
.todo-item.drag-over.drag-before::before { top: 0; }
.todo-item.drag-over.drag-after::after { bottom: 0; }

/* The same line for a whole project block being moved. */
.todo-project-group { position: relative; }
.todo-project-group.drag-over.drag-before::before,
.todo-project-group.drag-over.drag-after::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  z-index: 2;
}
.todo-project-group.drag-over.drag-before::before { top: -0.5rem; }
.todo-project-group.drag-over.drag-after::after { bottom: -0.5rem; }
/* FR#70: a thin line at the exact spot the card will land, instead of
   outlining the whole hovered card — drag-before/drag-after come from
   initDragReorder's axis:'x' tracking (mountProjectDragHandlers), so this
   lines up with the same left/right split its onDrop already decides on.
   Each pseudo-element only gets generated by its own combined selector, so
   exactly one line shows (never both, never one with no position). */
.project-card.drag-over.drag-before::before,
.project-card.drag-over.drag-after::after {
  content: '';
  position: absolute;
  top: 0.3rem;
  bottom: 0.3rem;
  width: 3px;
  border-radius: 2px;
  background: var(--accent);
  z-index: 2;
}
.project-card.drag-over.drag-before::before { left: -0.5rem; }
.project-card.drag-over.drag-after::after { right: -0.5rem; }

/* ---------- Lists (ideas, meetings, contacts, reminders) ---------- */
.list-item {
  background: var(--card-bg);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.7rem 0.9rem;
  margin-bottom: 0.5rem;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.6rem;
}
.list-item .content { flex: 1; }
.list-item .meta { font-size: 0.76rem; color: var(--muted); margin-top: 0.2rem; }
.list-item .actions { display: flex; gap: 0.3rem; flex-shrink: 0; }
/* FR#252: shared by both the Calendar page's .list-item rows and the
   sidebar Deadlines panel's .master-todo-item rows — a plain always-visible
   checkbox works in both since, unlike the to-do panel, neither has an icon
   occupying that spot to swap with on hover. */
.deadline-checkbox { flex-shrink: 0; width: 16px; height: 16px; margin-top: 0.2rem; cursor: pointer; }

/* ---------- Forms ---------- */
.form-row { margin-bottom: 0.9rem; }
.form-row label { display: block; font-size: 0.82rem; color: var(--muted); margin-bottom: 0.3rem; }
.form-row input, .form-row select, .form-row textarea {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
  font-family: inherit;
}
.form-row textarea { min-height: 80px; resize: vertical; }
/* FR#248: on a wide project page, a single-line field (a short title, a
   datetime picker, a URL) stretching edge-to-edge of the card looks
   broken — cap those specifically rather than the shared .form-row rule
   above, which textareas/rich editors/checkbox lists still want full-width. */
.form-row-narrow input, .form-row-narrow select { max-width: 420px; }
/* FR#192: a checkbox-plus-inline-text row is a different shape than every
   other .form-row (label above input) — without this it inherits
   .form-row label's block display (so the checkbox and its text just
   flow as normal inline content with no shared baseline/centering) and
   .form-row input's width:100%/padding meant for text fields, which
   stretches the checkbox and pads it off-center from its own label text. */
.form-row.event-deadline-row label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; color: var(--text); margin-bottom: 0; }
.form-row.event-deadline-row input[type="checkbox"] { width: auto; padding: 0; margin: 0; }
.form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
/* Project picker + Save note on one line (FR#58) instead of stacked rows. */
.quick-capture-actions-row { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0; }
.quick-capture-actions-row select { width: auto; flex: 1; min-width: 0; }
.quick-capture-actions-row button { flex-shrink: 0; }
/* FR#250: chips + a search-as-you-type dropdown, replacing a flat checkbox
   per contact — that stopped scaling once there were more than a handful. */
.attendee-picker { max-width: 420px; }
.attendee-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.4rem; }
.attendee-chips:empty { margin-bottom: 0; }
.attendee-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--accent-tint);
  color: var(--accent-dark);
  border-radius: 999px;
  padding: 0.25rem 0.4rem 0.25rem 0.7rem;
  font-size: 0.82rem;
}
.attendee-chip-remove {
  border: none;
  background: none;
  color: inherit;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.1rem 0.25rem;
  opacity: 0.7;
}
.attendee-chip-remove:hover { opacity: 1; }
.attendee-search-input { width: 100%; }
/* FR#101 established position:fixed + positionFloatingPanel for dropdowns
   that need to escape a clipping/scrolling ancestor — same reasoning here. */
.attendee-search-results {
  position: fixed;
  width: 280px;
  max-width: calc(100vw - 2rem);
  max-height: 240px;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.3rem;
  z-index: 100;
}
.attendee-search-result { padding: 0.45rem 0.6rem; border-radius: 6px; cursor: pointer; font-size: 0.88rem; }
.attendee-search-result:hover { background: var(--bg); }
.attendee-search-empty { padding: 0.45rem 0.6rem; font-size: 0.85rem; }
/* FR#139: lets a new attendee be created without leaving the meeting form. */
.new-attendee-row { display: flex; gap: 0.4rem; margin-top: 0.5rem; max-width: 420px; }
.new-attendee-row input { flex: 1; min-width: 0; }
.new-attendee-row button { flex-shrink: 0; }

/* FR#251: the whole header row (not just the chevron) expands/collapses a
   meeting card — edit/delete keep working since they carry their own
   data-action closer to the click than this row's. */
/* Held on one line beside the title: once the title was allowed to render
   on a phone (FR#332) it took the width it needed, and these two wrapped
   into a vertical stack down the side of the card. */
.meeting-card-actions { display: flex; flex-shrink: 0; align-items: flex-start; }
.meeting-card-header { cursor: pointer; border-radius: 8px; margin-left: -0.4rem; margin-right: -0.4rem; padding-left: 0.4rem; padding-right: 0.4rem; }
.meeting-card-header:hover { background: var(--bg); }
.meeting-expand-chevron { display: inline-block; width: 0.9em; color: var(--muted); font-size: 0.8em; }
.meeting-notes-preview { color: var(--muted); }
.link-btn {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-size: 0.85rem;
  padding: 0;
  margin-left: 0.3rem;
  white-space: nowrap;
}
.link-btn:hover { text-decoration: underline; }

/* ---------- Rich text editor (ideas) ---------- */
.rich-editor { margin-bottom: 0.9rem; }
/* Stays on one line: the note's own controls now ride at the end of this
   row (see noteDetailHtml), and wrapping put them on a second line —
   exactly the extra strip of chrome moving them here was meant to remove.
   The author's name is the flexible part and truncates instead. */
.rich-toolbar { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.2rem; margin-bottom: 0.4rem; }
.rich-toolbar > button, .rich-toolbar > label { flex: 0 0 auto; }
.rich-toolbar button {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  min-width: 30px;
  padding: 0.3rem 0.45rem;
  font-size: 0.85rem;
  cursor: pointer;
  line-height: 1;
}
.rich-toolbar button:active { background: var(--border); }
.rich-toolbar button.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.rich-editable {
  width: 100%;
  min-height: 80px;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  /* FR#402: scaled by the reader's own text-size setting (the Aa menu),
     which is a display preference — it never touches the stored HTML. */
  font-size: calc(0.95rem * var(--note-font-scale, 1));
  font-family: inherit;
  line-height: 1.4;
}
/* contenteditable gets a native focus outline like any other focusable
   element — FR#114: no focus ring anywhere, in any rich-editable context
   (quick capture, detail pane, fullscreen), not just Home's quick capture
   as originally scoped by FR#59. */
.rich-editable:focus { outline: none; }
.rich-editable:empty::before { content: attr(data-placeholder); color: var(--muted); }
.rich-editable ul, .rich-editable ol { margin: 0.3rem 0; padding-left: 1.4rem; }
/* FR#163: 1.4rem (22.4px) is only enough for a single digit plus the
   marker's own trailing gap — a 2-digit number like "11" needs to fit in
   that exact space too, which measured out tight enough to visibly crowd
   into the item's own text. ol gets extra room; ul's bullets don't grow
   with list length, so they're untouched. */
.rich-editable ol { padding-left: 2rem; }
/* FR#223: round rather than the browser's native (square-ish) checkbox —
   appearance:none hands all the rendering to this CSS, so checked state
   needs its own fill + checkmark drawn here instead of relying on the
   native tick. Clicking still toggles :checked normally either way. */
/* FR#443 took the non-breaking space out from after the checkbox, which
   left the caret drawn hard against the box. Asked for: put the gap back
   visually. Padding rather than a character, so the note's text still
   contains no space of its own — the caret sits after the box's own
   right-hand padding instead of after a space someone has to delete. */
.rich-editable input[type="checkbox"] {
  margin-right: 0.45em;
  appearance: none;
  -webkit-appearance: none;
  /* FR#429: rem, not em, and an explicit font-size. The meeting editor
     lives inside a <form>, so the general input styling applied to it and
     inflated the em these were sized in — the same checkbox came out twice
     as big in a meeting as in a note. rem is root-relative and immune to
     whatever the surrounding form does. */
  font-size: 1rem;
  width: 0.95rem;
  height: 0.95rem;
  min-width: 0;
  padding: 0;
  /* Centred on the line rather than hung off the baseline: at line-height
     1.6 a baseline offset leaves it sitting a few pixels high, which is
     exactly what reads as "the checkboxes are at the wrong height". */
  vertical-align: middle;
  position: relative;
  top: -0.06em;
  /* --border is tuned for dividers, which want to be barely there. An
     unchecked box is a control you have to find before you can hit it, so
     it gets a stronger edge of its own. */
  border: 1.5px solid var(--checkbox-border);
  /* Round here on purpose (FR#404), unlike the square to-do checkbox: this
     one matches the round checklist icon in the toolbar that inserts it. */
  border-radius: 50%;
  background: var(--card-bg);
  cursor: pointer;
  flex: none;
}
/* Gevraagd: een checklist hoort zich te gedragen als een lijstje. Een
   lijstje laat doorlopende tekst niet onder de bolletjes doorlopen maar
   netjes uitlijnen onder de woorden; zonder dit schoof de tweede regel van
   een lange checklist-regel weer helemaal naar links. Het hokje hangt in de
   marge, precies zoals een bullet dat doet. */
.rich-editable div:has(> input[type="checkbox"]:first-child) {
  padding-left: 1.4em;
  text-indent: -1.4em;
}
/* En inspringen met Tab, ook als een lijstje. Drie niveaus is genoeg om
   iets onder iets anders te hangen; dieper wordt een checklist onleesbaar. */
.rich-editable div.checklist-in-1 { margin-left: 1.4em; }
.rich-editable div.checklist-in-2 { margin-left: 2.8em; }
.rich-editable div.checklist-in-3 { margin-left: 4.2em; }
/* Gemeld: op een telefoon zijn de hokjes lastig te raken. Ze worden daar
   iets groter, en krijgen bovendien een onzichtbare rand eromheen die de
   tik opvangt — links, boven en onder ruim, naar rechts maar een beetje,
   want daar begint de tekst en daar wil je juist de cursor kunnen zetten.
   Wat daar net naast valt, vangt het script alsnog op (zie het tikgebied
   in app.js). */
@media (pointer: coarse) {
  .rich-editable input[type="checkbox"] {
    width: 1.15rem;
    height: 1.15rem;
    margin-right: 0.5em;
  }
  .rich-editable input[type="checkbox"]::before {
    content: '';
    position: absolute;
    top: -12px;
    bottom: -12px;
    left: -14px;
    right: -8px;
  }
  .rich-editable input[type="checkbox"]:checked::after { left: 5px; top: 2px; width: 5px; height: 9px; }
}

.rich-editable input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
.rich-editable input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 4px;
  top: 1px;
  width: 4px;
  height: 7px;
  border: solid #fff;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
/* FR#413: the first line of a note is its title, and used to be styled by
   ::first-line on the editor itself — which meant that turning the top of a
   note into a bulleted list made the first bullet big and bold, since it
   was then the first line of the block. That read as "lists make text
   bold". Scoped to a leading paragraph now, which is the only thing that
   was ever meant to be a title. */
.rich-editable > p:first-child::first-line,
.rich-editable > div:first-child::first-line { font-size: 1.15em; font-weight: 600; }
.rich-editable details {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.7rem;
  margin: 0.5rem 0;
}
.rich-editable details.dragging { opacity: 0.5; }
.rich-editable details.drag-over { outline: 2px solid var(--accent); outline-offset: 2px; }
.rich-editable summary { cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 0.3rem; }
.rich-editable summary::-webkit-details-marker { color: var(--muted); }
/* FR#273: display:flex above makes the browser skip the native marker
   entirely (it only renders for the default display:list-item), so the
   ::-webkit-details-marker rule above had nothing left to color — this is
   a real, separate arrow drawn with CSS instead, rotating via the details
   element's own [open] state rather than any JS. */
.rich-editable summary::before { content: '▸'; color: var(--muted); flex: none; transition: transform 0.15s; }
.rich-editable details[open] > summary::before { transform: rotate(90deg); }
.rich-editable .toggle-drag-handle { cursor: grab; color: var(--muted); touch-action: none; }
.rich-editable .toggle-title { flex: 1; min-width: 0; }
.rich-editable .toggle-title:empty::before { content: 'Toggle'; color: var(--muted); font-weight: normal; }
.rich-editable .toggle-delete {
  flex: none;
  border: none;
  background: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
  padding: 0.15rem 0.3rem;
  border-radius: 4px;
}
.rich-editable .toggle-delete:hover { background: var(--danger-tint, var(--border)); color: var(--danger); }
.rich-editable .toggle-body { margin-top: 0.4rem; padding-left: 1.2rem; font-weight: normal; }
.rich-editable .toggle-body:empty::before { content: 'Type here…'; color: var(--muted); }
.rich-editable img { max-width: 100%; border-radius: 8px; display: block; margin: 0.4rem 0; }
/* FR#176: width:100% (not just a max-width cap) so a table actually fills
   the editor instead of shrinking to its narrowest possible content width;
   the browser's default table layout still splits that width across
   columns by their own content ratios. */
/* FR#284: fixed (not the browser's default content-driven auto) so the
   <colgroup>/<col> widths normalizeTableColumns sets up are the actual
   source of truth for each column — auto would keep rebalancing columns
   by content regardless of what a drag handle just set. */
.rich-editable table { border-collapse: collapse; margin: 0.4rem 0; width: 100%; table-layout: fixed; }
.rich-editable td { border: 1px solid var(--border); padding: 0.3rem 0.5rem; min-width: 60px; position: relative; }
/* FR#201: th has no styling of its own by default beyond the browser's
   (centered, no border) — match td's box model and just add the bold/
   left-align/tint that actually reads as "header row" against it. */
.rich-editable th { border: 1px solid var(--border); padding: 0.3rem 0.5rem; min-width: 60px; font-weight: 700; text-align: left; background: var(--bg); position: relative; }
/* FR#284: a grab strip straddling the shared border between two columns —
   only ever present in the first row (see normalizeTableColumns), but a
   drag resizes the whole column via its <col>, not just that one cell. */
.rich-editable .col-resize-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  right: -4px;
  width: 8px;
  cursor: col-resize;
  z-index: 1;
  touch-action: none;
}
.rich-editable .col-resize-handle:hover, .rich-editable .col-resize-handle:active { background: var(--accent-tint); }
/* A heading gets real room beneath it, so the text under it reads as
   belonging to the heading rather than being crammed against it. The gap
   below is deliberately larger than the one above's own text — that
   asymmetry is what groups a heading with what follows it. */
.rich-editable h1, .rich-editable h2, .rich-editable h3 {
  font-weight: 700;
  margin: 1.1rem 0 0.65rem;
}
.rich-editable h1 { font-size: 1.5rem; }
.rich-editable h2 { font-size: 1.3rem; }
.rich-editable h3 { font-size: 1.12rem; }
/* Except the very first thing in a note, where a top margin just pushes the
   whole note down away from the toolbar. */
.rich-editable > h1:first-child,
.rich-editable > h2:first-child,
.rich-editable > h3:first-child { margin-top: 0; }
.rich-upload-btn {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  min-width: 30px;
  padding: 0.3rem 0.45rem;
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.rich-upload-btn:active { background: var(--border); }
.attachment-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: 0.85rem;
  text-decoration: none;
  color: var(--text);
}
.attachment-chip:hover { background: var(--border); }

/* ---------- Ideas ---------- */
.idea-item { flex-wrap: wrap; }
.idea-preview { min-width: 200px; cursor: pointer; border-radius: 8px; margin: -0.4rem; padding: 0.4rem; }
.idea-preview:hover { background: var(--bg); }
/* FR#102: this used to be `white-space: pre-wrap`, which preserved
   ideaSnippetHtml()'s own leading newline/indentation (just source
   formatting, not meaningful content) as a visible blank line above the
   title — pre-wrap made sense back when this held raw escaped-newline
   plain text directly, but title/preview now live in their own div lines
   below, so nothing here still needs literal whitespace preserved. */
.idea-title { font-weight: 600; }
.idea-preview-text { color: var(--muted); font-size: 0.85rem; margin-top: 0.15rem; }
.pin-badge { margin-right: 0.3rem; display: inline-flex; vertical-align: -0.15em; color: var(--accent); }
.pin-badge svg { width: 0.85em; height: 0.85em; }
/* No side padding of its own (FR#63) — sits flush with the same 1rem inset
   .topbar already gives every nav button, so the search box reads as the
   same width as the sidebar instead of narrower. */
.sidebar-search { position: relative; padding: 0 0 0.7rem; }
.notes-search-input {
  width: 100%;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.85rem;
  background: var(--bg);
  color: var(--text);
}
/* FR#101: position:fixed (JS-set left/top, see positionFloatingPanel)
   rather than position:absolute — the sidebar itself clips overflow-x for its
   own scrolling (see .topbar), which was cutting off the right side of this
   360px dropdown floating out of a 230px-wide sidebar. Fixed positioning
   escapes that clip the same way #context-menu already does. */
.sidebar-search-results {
  position: fixed;
  width: 360px;
  max-width: calc(100vw - 2rem);
  max-height: 70vh;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 0.6rem;
  z-index: 100;
}
.search-type-badge { margin-right: 0.2rem; }
.search-advanced-link { display: block; width: 100%; margin-top: 0.5rem; text-align: center; }

/* ---------- Advanced search page (FR#51) ---------- */
.search-page-controls { display: flex; flex-direction: column; gap: 0.7rem; }
.search-page-controls > input[type="text"] { font-size: 1rem; padding: 0.6rem 0.8rem; }
.search-page-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.search-type-toggle { display: flex; align-items: center; gap: 0.3rem; cursor: pointer; white-space: nowrap; }
.search-date-label { display: flex; align-items: center; gap: 0.4rem; white-space: nowrap; }
.search-date-label input[type="date"] { padding: 0.3rem 0.4rem; font-size: 0.82rem; }
#search-page-project { padding: 0.3rem 0.5rem; font-size: 0.85rem; }

/* ---------- Notes: master-detail layout (FR#39) ---------- */
/* A self-contained, fixed-height two-pane region (list left, open note
   right) rather than letting the page grow — same "lock the height, scroll
   internally" convention .app/.topbar/.right-panel already use, so a long
   note's content can never push the note list out of view. Below the
   860px breakpoint (where that whole convention doesn't apply — nav is a
   bottom bar, not a fixed-height sidebar) it just takes a generous
   viewport-relative height and stacks in normal document flow. */
.notes-master-detail {
  display: flex;
  overflow: hidden;
  height: 65vh;
  min-height: 360px;
}
@media (min-width: 860px) {
  /* FR#113/#230: this subtraction is only as good as the chrome height it
     was tuned against, and drifts stale whenever anything above it changes
     size. Had drifted to 77px against an actual 85px (.view-root's own
     top+bottom padding + .section-title's real height + margin, measured
     directly via getBoundingClientRect rather than assumed) — the 8px
     difference meant .notes-master-detail rendered 8px taller than the
     viewport actually had room for, giving the whole page a scrollbar with
     nothing meaningful to scroll. */
  .notes-master-detail { height: calc(100vh - 85px - var(--tab-strip-h, 0px)); }
  /* FR#69/#113: a project's own Notes tab has a much taller header above it
     (cover banner + timestamps + the section tab strip) than the
     standalone Notes page's plain title — the fixed subtraction above was
     tuned for the latter, so inside a project it needs its own (also
     re-measured for FR#113) value. Higher specificity here wins over the
     base rule above without needing !important. */
  .project-layout .notes-master-detail { height: calc(100vh - 264px - var(--tab-strip-h, 0px)); }
}
.notes-list-pane {
  flex: 0 0 var(--notes-list-width, 33%);
  overflow-y: auto;
  /* Asked for: room to the right of the list, so its rows and its
     scrollbar don't sit hard against the drag handle. */
  padding-right: 0.8rem;
}
/* FR#277: same visual language as .notes-search-input (the sidebar's own
   global search), scoped to this pane instead of full sidebar width. */
/* Search + the recently-deleted bin, at the top of the notes column. The
   bin sat in the page header next to "+ Nieuwe notitie", where it read as
   "delete this note" instead of "the notes I deleted" — it belongs with
   the list it opens. */
/* FR#339: search, compose and the bin stay at the top of the notes column
   while the list scrolls under them, rather than scrolling away and
   needing you back at the top to reach any of them. The list pane is the
   scroll container, so sticky is relative to that. Opaque, or note rows
   would show through as they pass beneath. */
.notes-list-toolbar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.6rem;
  position: sticky;
  top: 0;
  z-index: 4;
  /* FR#384: the same surface the page behind it uses, so the row reads as
     part of the page rather than a band laid over it. It has to be opaque —
     note rows scroll underneath — so "no background" means "the background
     already there". .view-root is --bg in dark and --card-bg in light (see
     the light-theme overrides near the top), which is why this needs both,
     exactly as .note-fullscreen .rich-toolbar already does. */
  background: var(--bg);
  padding-top: 0.35rem;
  padding-bottom: 0.35rem;
}
:root[data-theme="light"] .notes-list-toolbar { background: var(--card-bg); }
.notes-list-toolbar .notes-page-search-input { flex: 1 1 auto; min-width: 0; margin: 0; }

/* Gevraagd: filteren op project. Zolang het filter aanstaat zegt dit balkje
   waar je naar kijkt — anders lijkt een korte lijst een kwijtgeraakte
   notitie. Het kruisje zet het filter weer af. */
.notes-filter-balk {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
  padding: 0.25rem 0.3rem 0.25rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.85rem;
}
.notes-filter-balk .notes-filter-label {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.notes-filter-balk .muted { margin-left: auto; white-space: nowrap; }
.notes-filter-balk .icon-btn { padding: 0 0.35rem; line-height: 1; font-size: 1.1rem; }
.notes-list-toolbar .icon-btn { flex: 0 0 auto; }
.notes-page-search-input {
  display: block;
  width: 100%;
  margin: 0 0 0.6rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.85rem;
  background: var(--bg);
}
/* FR#231: was flex-basis 6px, plus .notes-list-pane's own 1px border-right
   immediately next to it — a thicker, slightly doubled-looking divider
   than intended. One thin line now; the hover/active accent color (below)
   still marks it as draggable without needing rest-state thickness to do
   that job. */
.notes-resizer {
  flex: 0 0 2px;
  cursor: col-resize;
  background: var(--border);
  touch-action: none;
}
.notes-resizer:hover, .notes-resizer:active { background: var(--accent); }
.notes-detail-pane { flex: 1; min-width: 0; height: 100%; }
.note-list-row {
  padding: 0.7rem 0.9rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.note-list-row:hover { background: var(--bg); }
.note-list-row.active { background: var(--accent-tint); }
.note-list-row-title {
  font-weight: 600;
  font-size: 0.92rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.note-list-row-preview {
  color: var(--muted);
  font-size: 0.85rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 0.1rem;
}
.note-list-row-meta { color: var(--muted); font-size: 0.76rem; margin-top: 0.3rem; }
.note-list-subheading {
  padding: 1.3rem 0.9rem 0.2rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

/* The note editor shown in the detail pane above — kept the `.note-fullscreen`
   name from when this was its own full-viewport route (`#note/ID`); most
   autosave/checkbox/toggle-drag logic in app.js still targets notes via
   `.closest('.note-fullscreen')`, so renaming it would mean touching every
   one of those call sites for no visual benefit. */
.note-fullscreen {
  height: 100%;
  display: flex;
  flex-direction: column;
  /* No top padding: the sticky toolbar carries it instead. With padding
     here, the bar started 0.7rem below the top of the scroll area and
     snapped up to it on the first scroll — an 11px jump every time you
     began scrolling. Moving that space into the toolbar's own box makes
     where it starts and where it sticks the same place. */
  padding: 0 0.7rem 0.7rem;
}
.note-save-status { font-size: 0.8rem; min-width: 3.5rem; }
/* FR#256: sits in the meeting edit form's .form-actions (flex,
   justify-content:flex-end) — margin-right:auto claims the row's leftover
   space so the status reads on the left and Save stays pinned right. */
.meeting-save-status { font-size: 0.8rem; margin-right: auto; }
/* min-height: 0 lets this shrink to its fair share of .note-fullscreen's
   fixed height instead of growing with content (the usual flex/min-height
   default); .rich-editable then scrolls internally on its own, so a long
   note scrolls just the text while the toolbar/header stay put. */
/* FR#217: .rich-editor's own base rule (below) sets a 0.9rem margin-bottom
   meant for standalone forms where something follows it — nothing does
   here, so left alone it was pure unused space under the editable area on
   top of .note-fullscreen's own padding. */
/* The editable used to be flex:1, stretching to the pane's full height and
   scrolling internally. That meant a three-line note sat in a box some 500px
   taller than its own text — the space Sam flagged. It sizes to its content
   now and the pane does the scrolling instead, so a note ends where its text
   ends. min-height keeps a new, empty note from collapsing to a sliver, and
   the click handler in app.js keeps "click below the text to keep typing"
   working now that the editable no longer covers that area itself. */
.note-fullscreen .rich-editor { flex: 0 0 auto; display: flex; flex-direction: column; margin-bottom: 0; }
.note-fullscreen .rich-editable { flex: 0 0 auto; min-height: 6rem; overflow: visible; font-size: calc(0.92rem * var(--note-font-scale, 1)); line-height: 1.6; border: none; padding: 0.4rem 0.1rem; }
/* Scrolling moves out here, so long notes still scroll — just the pane
   rather than a nested box. */
.notes-detail-pane { overflow-y: auto; }
/* Claims the leftover height so a click anywhere under the text still lands
   on something that can put the caret back in the note. */
.note-fullscreen { cursor: text; }
/* FR#180: sits in normal document flow right after the block/line that
   contained its link (see mountInlineVideoEmbeds) rather than grouped with
   every other embed at the end of the note — so unlike the old bottom-of-
   note stack (FR#161) it never needs its own height cap/scroll region, just
   the handful of links that happen to share one line. contenteditable=false
   keeps it out of the way of typing/backspace, same island pattern as the
   toggle block's own drag handle/delete button. */
.video-embed-inline { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.5rem 0; cursor: default; }
.video-embed { width: 100%; max-width: 400px; aspect-ratio: 16 / 9; border-radius: 10px; overflow: hidden; background: #000; flex-shrink: 0; }
.video-embed iframe { width: 100%; height: 100%; border: none; display: block; }

/* ---------- Deadlines / timeline .ics subscribe ---------- */
.ics-subscribe { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; font-size: 0.85rem; }
.ics-subscribe input[type="text"] {
  flex: 1;
  min-width: 180px;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--bg);
}
/* FR#157's urgency border here went with the ranking itself (FR#396). */
.deadlines-list .list-item { cursor: pointer; }
/* FR#177: the whole row is clickable (data-action="calendar-open-item" on
   the root element), not just its urgency border — cursor/hover feedback
   was missing entirely, matching the same var(--bg) hover already used
   for .note-grid-card/.todo-title elsewhere. */
.deadlines-list .list-item:hover { background: var(--bg); }
/* FR#224: .list-item's own margin-bottom (for spacing *between* items) left
   a trailing 0.5rem after the last one here, stacking with the next
   .section-title's 1.4rem top margin — every other Home section (grids/flex
   using their own gap, no per-item margin) only ever got that 1.4rem, so
   the gap before the next heading read taller than the gaps between other
   headings. Dropping just the last item's own margin here (not .list-item's
   in general — it's shared well beyond Home) makes every inter-section gap
   the same 1.4rem. */
.home-unfiled-notes .list-item:last-child { margin-bottom: 0; }

/* ---------- Time tracking ---------- */
.time-manager-filters { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.time-manager-filters select { padding: 0.4rem 0.6rem; height: 2rem; border: 1px solid var(--border); border-radius: 8px; font-size: 0.85rem; }
.project-link { cursor: pointer; color: var(--accent-dark); text-decoration: underline; }
.contact-name-link { cursor: pointer; }
.contact-name-link:hover { color: var(--accent-dark); text-decoration: underline; }
.contact-projects { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.3rem; font-size: 0.8rem; }
/* FR#213: icon-only here (name shown via the span's own title attribute
   instead) — the base .project-link underline is for the Time tracker's
   icon+text usage and would sit oddly under a bare icon. */
.contact-projects .project-link { display: inline-flex; text-decoration: none; }
.contact-projects .project-link:hover { opacity: 0.8; }
.time-summary { display: flex; gap: 1.2rem; margin-bottom: 1rem; flex-wrap: wrap; }
.time-summary .stat { text-align: center; }
.time-summary .stat .num { font-size: 1.3rem; font-weight: 700; }
.time-summary .stat .label { font-size: 0.75rem; color: var(--muted); }
.time-controls { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.rate-form { display: flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; color: var(--muted); }
.rate-form label { font-size: 0.75rem; }
.rate-form input[type="number"] { width: 4.5rem; padding: 0.25rem 0.4rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; }
.start-timer-form { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.start-timer-form input {
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.88rem;
  min-width: 220px;
}
.inline-edit-row { display: flex; align-items: center; gap: 0.5rem; flex: 1; flex-wrap: wrap; }
.inline-edit-row input {
  flex: 1;
  min-width: 160px;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.88rem;
}

/* ---------- Fireworks ---------- */
.fireworks-overlay { position: fixed; inset: 0; z-index: 9999; pointer-events: none; overflow: hidden; }
.firework-particle {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  animation: firework-burst 0.9s ease-out forwards;
}
@keyframes firework-burst {
  0% { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) scale(0.2); opacity: 0; }
}

/* ---------- Toast ---------- */
.toast-root { position: fixed; bottom: calc(90px + var(--safe-bottom)); left: 0; right: 0; display: flex; flex-direction: column; align-items: center; gap: 0.4rem; z-index: 600; pointer-events: none; }
.toast {
  background: #1f2330; color: #fff; padding: 0.6rem 1rem; border-radius: 8px;
  font-size: 0.85rem; box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* FR#320: .toast-root is pointer-events:none so a toast never blocks the
     page underneath it — but nothing turned it back on for the toast
     itself, which meant "Ongedaan maken" could be seen and never clicked.
     Re-enabled here, on the toast only, so the empty space around it stays
     click-through. */
  pointer-events: auto;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast-leaving { opacity: 0; transform: translateY(6px); }
.toast-undo {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: #a5b4fc;
  cursor: pointer;
  white-space: nowrap;
}
.toast-undo:hover { text-decoration: underline; }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}
@media (min-width: 860px) { .toast-root { bottom: 20px; } }

/* ---------- Urenregel bewerken ---------- */
.time-entry-edit { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.5rem; width: 100%; }
.time-entry-edit > input[name="description"] { flex: 1 1 14rem; min-width: 0; }
.time-entry-edit-veld { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.75rem; color: var(--muted); }
.time-entry-edit-veld input { font-size: 0.9rem; }
.time-entry-edit-knoppen { display: flex; gap: 0.4rem; margin-left: auto; }

/* ---------- Trekken om te vernieuwen (To do's-app) ---------- */
.pull-refresh {
  --trek: 0px;
  position: fixed; left: 0; right: 0; margin: 0 auto; width: max-content;
  top: calc(env(safe-area-inset-top, 0px) + 8px); z-index: 1300;
  display: flex; align-items: center; gap: 0.45rem;
  padding: 0.4rem 0.85rem; border-radius: 999px;
  background: var(--card-bg); color: var(--muted); font-size: 0.82rem; font-weight: 600;
  border: 1px solid var(--border); box-shadow: 0 6px 18px rgba(20, 20, 40, 0.14);
  opacity: 0; pointer-events: none;
  transform: translateY(calc(var(--trek) - 56px));
  transition: opacity 120ms ease;
}
.pull-refresh.is-zichtbaar { opacity: 1; }
.pull-refresh.is-klaar { color: var(--accent); }
.pull-refresh-pijl { display: inline-block; transition: transform 150ms ease; }
.pull-refresh.is-klaar .pull-refresh-pijl { transform: rotate(180deg); }
.pull-refresh.is-bezig .pull-refresh-pijl { animation: pull-refresh-draai 0.8s linear infinite; }
@keyframes pull-refresh-draai { to { transform: rotate(540deg); } }
@media (prefers-reduced-motion: reduce) {
  .pull-refresh, .pull-refresh-pijl { transition: none; }
  .pull-refresh.is-bezig .pull-refresh-pijl { animation: none; }
}

/* ---------- Melding: nieuwe versie (desktop) ---------- */
.update-popup {
  position: fixed; right: 20px; bottom: 20px; z-index: 1200;
  display: flex; align-items: center; gap: 1rem;
  max-width: 420px; padding: 0.9rem 1rem;
  background: var(--card-bg); color: var(--text);
  border: 1px solid var(--border); border-left: 4px solid var(--accent);
  border-radius: var(--radius); box-shadow: 0 10px 30px rgba(20, 20, 40, 0.18);
  animation: update-popup-in 180ms ease-out;
}
.update-popup-text { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.88rem; }
.update-popup-text span { color: var(--muted); }
.update-popup-actions { display: flex; gap: 0.4rem; flex: none; }
@keyframes update-popup-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .update-popup { animation: none; } }

/* ---------- Context menu ---------- */
.context-menu {
  position: fixed;
  z-index: 700;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow);
  padding: 0.3rem;
  min-width: 170px;
  display: flex;
  flex-direction: column;
}
.context-menu.hidden { display: none; }
.context-menu-heading {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  padding: 0.5rem 0.6rem 0.25rem;
}
/* No leading gap above the very first group. */
.context-menu-heading:first-child { padding-top: 0.25rem; }
.context-menu button {
  background: none;
  border: none;
  text-align: left;
  padding: 0.5rem 0.7rem;
  border-radius: 6px;
  font-size: 0.88rem;
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.context-menu button svg { width: 1.05em; height: 1.05em; flex-shrink: 0; }
.context-menu button:hover { background: var(--bg); }
.context-menu button.danger:hover { background: var(--danger-tint); color: var(--danger); }
/* FR#218: the Aa/list formatting popup reuses this same menu, so its
   active-formatting highlight (bold/italic/etc. already applied at the
   caret) needs the same treatment .rich-toolbar button.active gets. */
.context-menu button.active { background: var(--accent); color: #fff; }

/* status badges */
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.72rem; font-weight: 600; }
.badge.active { background: var(--success-tint); color: var(--success); }
.badge.archived { background: var(--neutral-tint); color: var(--muted); }
.badge.label { background: var(--accent-tint); color: var(--accent-dark); }
.badge.funding-planned { background: var(--neutral-tint); color: var(--muted); }
.badge.funding-submitted { background: var(--warn-tint); color: var(--warn); }
.badge.funding-awarded { background: var(--success-tint); color: var(--success); }
.badge.funding-declined { background: var(--danger-tint); color: var(--danger); }
/* FR#140 */
.funding-totals { display: flex; gap: 1.2rem; font-size: 0.9rem; margin-bottom: 0.8rem; }
.funding-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0; }
.funding-toolbar select { width: auto; }
/* FR#210 */
.funding-attachments { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: 0.4rem; }
.funding-attachment-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.3rem 0.15rem 0.6rem;
  font-size: 0.78rem;
}
.funding-attachment-chip a { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--text); text-decoration: none; }
.funding-attachment-chip a:hover { color: var(--accent-dark); }
.funding-attachment-chip svg { width: 0.95em; height: 0.95em; flex-shrink: 0; }
.funding-attachment-remove { background: none; border: none; cursor: pointer; color: var(--muted); padding: 0.1rem 0.3rem; font-size: 0.75rem; line-height: 1; }
.funding-attachment-remove:hover { color: var(--danger); }
.funding-attachment-add {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.78rem;
  color: var(--muted);
  cursor: pointer;
  border: 1px dashed var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
}
.funding-attachment-add:hover { color: var(--text); border-color: var(--accent); }
.funding-attachment-add svg { width: 0.95em; height: 0.95em; }

a { color: var(--accent-dark); }

/* ---------- Pomodoro ---------- */
/* FR#171: scoped to :has(.pomodoro-page) rather than centering .view-root
   generally, so every other route's normal top-aligned, scrollable layout
   is untouched — this only kicks in for the one page that's a single fixed-
   size widget with nothing else competing for the space above/below it. */
.view-root:has(.pomodoro-page) { display: flex; align-items: center; justify-content: center; }
.pomodoro-page { display: flex; flex-direction: column; align-items: center; padding: 1rem 0 2rem; }
.pomodoro-modes { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; justify-content: center; }
.pomodoro-mode-btn {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  cursor: pointer;
  color: var(--text);
  font-size: 0.85rem;
}
.pomodoro-mode-btn.active { background: var(--danger); color: #fff; border-color: var(--danger); }
.pomodoro-ring-wrap { position: relative; width: 260px; height: 260px; margin-bottom: 1.5rem; max-width: 80vw; }
.pomodoro-ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.pomodoro-ring-bg { fill: none; stroke: var(--border); stroke-width: 12; }
.pomodoro-ring-progress { fill: none; stroke: var(--danger); stroke-width: 12; stroke-linecap: round; transition: stroke-dashoffset 1s linear; }
.pomodoro-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.pomodoro-tomato { font-size: 2.6rem; }
.pomodoro-time { font-size: 2.2rem; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 0.2rem; }
.pomodoro-controls { display: flex; gap: 0.8rem; }

/* ======================================================================
   FR#262: mobile-only UX changes, all gathered here rather than scattered
   next to each desktop rule they override — additive-only (nothing here
   touches an existing declaration), so every one of these is guaranteed
   to have zero effect at the ≥860px desktop breakpoint used everywhere
   else in this file.
   ====================================================================== */
/* Same specificity as the display overrides inside the media query below —
   these have to come first in source order so the cascade tie resolves in
   the media query's favor once it's active (mobile), while still being the
   only rule in effect (hidden) when it isn't (desktop). .subtabs-mobile-
   trigger additionally needs the "button" element type in its selector
   (matching .subtabs button's own shape) since it's a <button> inside
   .subtabs, same specificity gotcha as .subtab-add above. */
.subtabs button.subtabs-mobile-trigger, .notes-mobile-back, .subtabs-mobile-row { display: none; }
/* FR#338: iOS zooms the whole page in when you focus a text field whose
   font is under 16px, and never zooms back out — which is why typing
   anywhere left the layout enlarged. 16px is the threshold, not a
   preference: at 15.9px it still zooms. Phones only; the desktop keeps the
   smaller control text. */
@media (max-width: 859.98px) {
  input:not([type="checkbox"]):not([type="radio"]), select, textarea {
    font-size: 16px;
  }
}

/* FR#340: kills the double-tap-to-zoom gesture, which on a touch device
   also removes the ~300ms wait browsers otherwise keep before treating a
   tap as a click. */
body {
  touch-action: manipulation;
}

@media (max-width: 859.98px) {
  /* Home's Recent notes: a compact list (title + project/date on the
     right) instead of the square preview-card grid — same markup
     (noteCarouselCardHtml), just the preview body text hidden and the
     card's own background/shadow/padding stripped so it reads as a plain
     row rather than a tile. */
  .notes-carousel { display: block; }
  .note-carousel-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.65rem 0;
    border-bottom: 1px solid var(--border);
  }
  .note-carousel-item:last-child { border-bottom: none; }
  .note-carousel-item .note-grid-card {
    aspect-ratio: auto;
    background: none;
    box-shadow: none;
    padding: 0;
    margin-bottom: 0;
    flex: 1;
    min-width: 0;
  }
  .note-carousel-item .note-grid-card .idea-preview-text { display: none; }
  .note-carousel-caption { flex-shrink: 0; text-align: right; }

  /* Projects page (and, per FR#281, Home's "Recent projects" too): one row
     per project instead of a multi-column grid of square tiles — same card
     (cover image + scrim + name + favorite star + today badge), just
     shortened into a full-width strip so more fit on screen without
     scrolling. */
  .project-grid { display: flex; flex-direction: column; gap: 0.6rem; }
  .project-grid .project-card { aspect-ratio: auto; height: 64px; }
  /* FR#281 capped this to 5 on the phone, back when it listed recently
     touched projects and five was a reasonable window onto a list that
     reordered itself anyway. It shows favourites now — a list you chose —
     and asked for: no maximum. A starred project that isn't on the screen
     meant to show starred projects is the whole feature failing. */

  /* Project page header: never the cover-photo background, always short —
     background-image needs !important since the photo/gradient itself is
     an inline style (per-project, from the DB) that only !important in a
     stylesheet can out-rank. */
  .detail-header.has-cover { background-image: none !important; padding: 0; }
  .detail-header-scrim { display: none; }
  .detail-header.has-cover .detail-header-content { color: var(--text); }
  .detail-header.has-cover .icon-btn { color: inherit; }
  .detail-header.has-cover .icon-btn:hover { background: var(--bg); }
  /* FR#296: the icon/title/code/created-activity block is dropped
     entirely on mobile (not just de-styled, per the has-cover rules
     above, which now only matter for the still-visible ☰ button) —
     the topbar's own back arrow + page title already say which project
     this is, so repeating it here was pure scroll with nothing new in
     it. Renaming and changing the cover are still reachable from ☰'s
     menu (see open-project-settings-menu in app.js), which is why
     unlike the rest of this block, that button stays. */
  /* h1 needs the [data-action] in its selector to match .detail-header
     h1[data-action]'s own specificity above (which sets display:inline-
     block), leaving source order to settle it — same tie-breaking shape
     as .notes-page-header-actions.note-editing further down. */
  .detail-header-icon-btn, .detail-header-content h1[data-action], .project-code-row, .project-timestamps { display: none; }

  /* Project page section nav: the inline list (.subtabs' own buttons)
     hides in favor of the one hamburger trigger viewProjectDetail renders
     alongside it (see subtabsMobileTriggerHtml) — same section-switch/add
     actions, just reached through a popup instead of a cluttered strip of
     buttons that would otherwise need horizontal scrolling on a phone. */
  /* Sections are a swipeable strip of tabs rather than a dropdown: every
     section visible as its own tab, the current one underlined, and the
     rest reached by scrolling the strip sideways. A dropdown hid where you
     could go behind a tap; this shows it.

     The strip scrolls rather than wrapping, so the row never grows taller,
     and the scrollbar is hidden since the tabs themselves are the cue. */
  .subtabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    gap: 0.1rem;
  }
  .subtabs::-webkit-scrollbar { display: none; }
  .subtabs-mobile-row { display: none; }
  .subtab-row { display: flex; flex: 0 0 auto; }
  .subtabs button.subtab-add { display: inline-flex; flex: 0 0 auto; }
  .subtabs button {
    border-radius: 0;
    border-bottom: 2px solid transparent;
    padding: 0.55rem 0.7rem;
    white-space: nowrap;
  }
  /* The active tab is marked by its underline, the way the tab strips this
     is modelled on do it — not by a filled background, which at this size
     reads as a button you press rather than the page you're on. */
  .subtab-row button.active {
    background: none;
    color: var(--accent-dark);
    border-bottom-color: var(--accent);
    font-weight: 600;
  }

  /* Notes master-detail (tabIdeas()/viewNotes()): one pane at a time
     instead of side-by-side — the list fills the width by default; opening
     a note (state.ui.mobileNoteOpen, see the .mobile-detail-open class)
     swaps to the detail pane full-width instead, with .notes-mobile-back
     to return. The resizer drag-handle has nothing to resize between two
     panes that are never both on screen together. */
  .notes-resizer { display: none; }
  .notes-list-pane { flex: 1 1 100%; }
  .notes-detail-pane { display: none; }
  .notes-mobile-back {
    display: block;
    background: none;
    border: none;
    color: var(--accent);
    font-size: 0.9rem;
    font-weight: 600;
    padding: 0.4rem 0;
    margin-bottom: 0.4rem;
    cursor: pointer;
  }
  /* The note slides OVER the list rather than replacing it. The list used
     to be display:none'd the moment a note opened, so the pane slid in
     across empty space and the list appeared to vanish first. Keeping it
     in place underneath — with the detail pane laid over it and opaque —
     is what makes the movement read as one layer sliding across another. */
  .notes-master-detail.mobile-detail-open { position: relative; }
  .notes-master-detail.mobile-detail-open .notes-list-pane { display: block; }
  /* FR#397: on a phone the notes toolbar leaves the top of the list and
     becomes a floating bar at the bottom of the screen — search, then the
     ☰ menu, then compose, iPhone-Notes style. It sits above the bottom nav
     (or above the home indicator alone in the standalone Notes app, which
     has no nav — see the [data-solo] override below). */
  .notes-list-toolbar {
    position: fixed;
    left: 0.5rem;
    right: 0.5rem;
    bottom: calc(70px + var(--safe-bottom) + 0.4rem);
    top: auto;
    z-index: 19;
    margin: 0;
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 16px;
    background: var(--card-bg);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);
    /* Note rows pass under it, so the surface stays opaque — the blur is
       only there to keep the edge from reading as a flat slab. */
    -webkit-backdrop-filter: saturate(1.4) blur(8px);
    backdrop-filter: saturate(1.4) blur(8px);
  }
  :root[data-theme="light"] .notes-list-toolbar { background: var(--card-bg); }
  /* Inside the bar the field is a pill on the bar's own surface, not a
     second bordered box drawn inside the first. */
  .notes-list-toolbar .notes-page-search-input {
    border-color: transparent;
    border-radius: 999px;
    background: var(--bg);
    padding-left: 0.9rem;
  }
  :root[data-theme="light"] .notes-list-toolbar .notes-page-search-input { background: var(--hover-bg, rgba(0, 0, 0, 0.05)); }
  /* The bar no longer holds a place in the flow, so the list has to end
     above it rather than run underneath the last row. */
  .notes-master-detail .notes-list-pane { padding-bottom: 4.2rem; }
  /* The standalone Notes app has no bottom nav to clear (see
     :root[data-solo] .app), so the bar drops to the safe area itself. */
  :root[data-solo] .notes-list-toolbar { bottom: calc(var(--safe-bottom) + 0.5rem); }
  /* The keyboard covers the bottom of the screen; a bar pinned there would
     be behind it. Searching pulls it back up to the top of the list, where
     it was before FR#397. */
  :root[data-keyboard="open"] .notes-list-toolbar {
    position: sticky;
    top: 0;
    bottom: auto;
    left: auto;
    right: auto;
    box-shadow: none;
  }
  :root[data-keyboard="open"] .notes-master-detail .notes-list-pane { padding-bottom: 0; }

  /* The list's toolbar is sticky with a z-index (FR#339), which put it
     above the note sliding over the list — so search, compose and the bin
     stayed on screen on top of an open note. They belong to the overview,
     so they go with it.
     FR#379: hidden with visibility, not display. Taking it out of the flow
     collapsed its height, so the list underneath shifted up while the note
     was open and jumped back down as it closed — the "search bar loading
     later" effect. The note covers the list completely anyway, so the
     reserved space costs nothing and the geometry never changes. */
  .notes-master-detail.mobile-detail-open:not(.closing) .notes-list-toolbar { visibility: hidden; }
  /* An open note is the whole screen on a phone; the padding that frames it
     as a pane beside a list is just margin eating the text column. */
  .notes-master-detail.mobile-detail-open .note-fullscreen { padding: 0.35rem 0.5rem; }
  .notes-master-detail.mobile-detail-open .note-fullscreen .rich-editable { padding: 0.3rem 0; }
  .notes-master-detail.mobile-detail-open .notes-detail-pane {
    display: block;
    position: absolute;
    inset: 0;
    overflow-y: auto;
    /* Matches whatever is behind it: light paints .view-root with
       --card-bg, dark leaves it unpainted so the page shows --bg. Using
       --card-bg for both made the open note lighter than the page in dark
       mode — the "different colour" on phones. */
    background: var(--bg);
    z-index: 2;
  }
  :root[data-theme="light"] .notes-master-detail.mobile-detail-open .notes-detail-pane { background: var(--card-bg); }

  /* FR#297: a note slides in from the right when opened and back out to
     the right when closed, so the list feels like it stays put underneath
     rather than both panes hard-cutting. Applied by class rather than to
     the pane itself: the slide-in only plays on the render that actually
     opens a note (consumeNoteSlideInClass in app.js), and the slide-out
     is awaited before the closing render tears the pane down. */
  @keyframes note-slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
  @keyframes note-slide-out { from { transform: translateX(0); } to { transform: translateX(100%); } }
  /* FR#393: the pane under the finger during a back-swipe. No transition
     while dragging — it should sit exactly where the finger is — and one
     while settling, home or off the edge. */
  .notes-detail-pane.note-back-dragging {
    transform: translateX(var(--note-back-x, 0px));
    transition: none;
  }
  .notes-detail-pane.note-back-settling {
    transform: translateX(var(--note-back-x, 0px));
    transition: transform 200ms ease-out;
  }
  .notes-detail-pane.note-slide-in { animation: note-slide-in 220ms ease-out; }
  .notes-detail-pane.note-slide-out { animation: note-slide-out 220ms ease-in forwards; }
  /* The close handler waits on animationend, which never fires if the
     animation is suppressed outright — keep a ~0-length one so it still
     resolves promptly instead of falling through to its timeout. */
  @media (prefers-reduced-motion: reduce) {
    .notes-detail-pane.note-slide-in, .notes-detail-pane.note-slide-out { animation-duration: 1ms; }
    .notes-detail-pane.note-back-settling { transition-duration: 1ms; }
  }

  /* FR#278: the topbar brand only says "Project Manager 2000" on Home —
     elsewhere it shows that page's own title instead (project name, or
     "Notes", "Contacts", etc. — see updateTopbarPageTitle in app.js),
     since the page's own heading already does that job on desktop but
     mobile has no room to show both a generic app title and a page
     heading. :has() lets this stay pure CSS off of whether app.js actually
     put text in .topbar-page-title, rather than needing a second JS-toggled
     class for the same on/off state. */
  .topbar-brand:has(.topbar-page-title:not(:empty)) .topbar-brand-text { display: none; }
  .topbar-brand:has(.topbar-page-title:not(:empty)) .topbar-page-title { display: inline; font-size: 0.95rem; }

  /* FR#296: a back arrow next to that page title, everywhere except Home
     (app.js's updateTopbarPageTitle toggles .hidden off/on) — takes you to
     whichever page was open right before this one (previousRouteHash in
     app.js), not a fixed "up one level" destination. */
  .topbar-back-btn:not(.hidden) { display: inline-flex; align-items: center; }
  .label-short { display: inline; }
  .label-long { display: none; }
  /* FR#345: the logo and the back arrow are both "where am I" cues, and two
     of them side by side just crowds a narrow bar. The arrow wins wherever
     it appears — which is everywhere except the homepage, so the logo ends
     up being the homepage's own marker. */
  .topbar:has(.topbar-back-btn:not(.hidden)) .topbar-brand .nav-icon { display: none; }

  /* FR#279: the note editor's project <select> has no room next to the ☰
     menu on a phone-width header — "Move to project" in that menu (see
     open-note-move-menu in app.js) replaces it there. .note-editing is
     repeated here (matching .notes-page-header-actions select's own
     selector at the top of this file) purely to win that equal-specificity
     tie, since source order is all that decides it otherwise. */

  /* FR#298: a short page (the to-do board especially — three stacked
     columns that are mostly headers once a column or two is empty) left
     the content pane ending mid-screen, with the gray page background
     showing through from there down to the bottom nav. Desktop already
     avoids this by flexing .view-root to fill (see the ≥860px block);
     mobile kept .app as a plain block, so it never stretched. Only the
     topbar and .view-root are in flow here — .right-panel is display:none
     and .bottom-nav is fixed — so this just makes the pane take the slack.
     .app's own padding-bottom still reserves the nav's height. */
  .app { display: flex; flex-direction: column; }
  .main-column { flex: 1; }
  .view-root { flex: 1; }

  /* FR#282: the topbar's "start a timer" affordance is just the pill in its
     idle state — redundant clutter now that the Time tracker page has its
     own Start button (see globalTimerButtonHtml in viewTimeManager). Only
     while idle: a timer actively running stays visible here too, since
     phone still benefits from seeing/stopping it from anywhere. */
  #running-timer-pill.idle { display: none; }
}
.pomodoro-sessions { margin-top: 1.2rem; font-size: 0.85rem; }

/* FR#151: the initial of whoever added an item, shown only on other
   people's work (see attributionHtml in app.js) — a small neutral chip
   rather than a colour-coded avatar, since it sits inline with titles
   that already carry urgency colour of their own. */
/* FR#330: who a to-do is for, top-right of the card. Deliberately louder
   than .attribution-initial (who wrote it): an assignment is something you
   scan a board for, an author is something you check. */
/* FR#351: the ☰ that replaces long-press on a touch screen. Desktop has
   right-click and doesn't need it taking up room in every row. */
/* FR#360: meeting rows in the master-detail list. Deliberately NOT
   .note-list-row — that class carries the swipe-to-delete handler, which
   would delete a note by the meeting's id. Same look, its own name. */
.meeting-list-row {
  padding: 0.55rem 0.6rem;
  border-radius: 8px;
  cursor: pointer;
  margin-bottom: 0.15rem;
}
.meeting-list-row:hover { background: var(--bg); }
.meeting-list-row.active { background: var(--accent-tint); }
.meeting-list-row.done .meeting-list-row-title { text-decoration: line-through; opacity: 0.6; }
.meeting-list-row-title { font-weight: 600; font-size: 0.9rem; }
.meeting-list-row-meta { font-size: 0.78rem; color: var(--muted); margin-top: 0.15rem; }
.meeting-detail { padding: 0.2rem 0.1rem; }
/* Asked for: date, time, kind, who was there and whether it happened all
   share one row, so the writing area starts near the top the way a note's
   does. It still wraps — on a phone this is two or three lines rather than
   one — but it is one band of metadata instead of a stack of fields. */
.meeting-meta-row { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: flex-end; }
/* Asked for: attendees on their own line below the row, not competing
   with four other fields in it — and a quarter of the width, since a name
   is short and the field was spanning the whole pane for no benefit. */
.meeting-attendee-row { margin-top: 0.2rem; }
/* Deelnemers staan op hun eigen regel, dus die regel is er ook helemaal
   voor. Op een kwart breedte paste er één naam naast en viel de rest
   eronder, waardoor een meeting met vier mensen een kolom werd. */
.meeting-attendee-row .attendee-picker { width: 100%; max-width: none; }
/* Asked for: "Gehad" reads as a label above its control, like the fields
   beside it, and the control is the same height as they are so the row
   lines up. A checkbox of that height would be enormous, so it is the box
   around it that matches — the checkbox sits centred inside. */
.meeting-meta-row .meeting-took-place-row { flex: 0 0 auto; }
.meeting-meta-row .meeting-took-place {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 2.6rem;
  /* The same height token the date, time and kind fields use, so the row
     lines up by construction rather than by a number copied here that
     would drift the moment theirs changed. */
  height: var(--meeting-field-h);
  padding: 0 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg);
  box-sizing: border-box;
  cursor: pointer;
  margin: 0;
}
.meeting-meta-row .meeting-took-place input { width: 16px; height: 16px; margin: 0; }
.meeting-meta-row .meeting-took-place input { margin: 0; cursor: pointer; }
/* min-width:0 so these can actually shrink: a flex item defaults to its
   content's minimum width, and a date input's intrinsic width is wide
   enough that three of them wrapped onto three separate lines in a narrow
   detail pane — 218px of metadata above a writing area that was then left
   with almost nothing. */
.meeting-meta-row .form-row { flex: 1 1 6rem; min-width: 0; margin-bottom: 0.4rem; }
/* The attendee picker stacks three controls of its own (chips, a search
   box, and an add-someone-new row). Side by side they cost one line
   instead of three, which is the difference between the writing area
   starting on screen and starting below the fold. */
.meeting-detail .attendee-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.meeting-detail .attendee-picker .attendee-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.meeting-detail .attendee-picker .attendee-search-input { flex: 1 1 10rem; width: auto; }
.meeting-detail .attendee-picker .attendee-search-results { flex: 1 1 100%; }
.meeting-detail .form-row { margin-bottom: 0.5rem; }
/* The "add this name" row at the bottom of the attendee results. */
.attendee-search-create { font-weight: 600; color: var(--accent-dark); }

/* Whether the meeting happened. Reads as a statement about the meeting
   rather than as another labelled field, so it sits inline. */
.meeting-took-place {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0.1rem 0 0.6rem;
  cursor: pointer;
  user-select: none;
}
.meeting-took-place input { width: auto; margin: 0; flex: none; }

/* Asked for: the writing area takes whatever height is left. The detail
   pane already has a real height (the master-detail layout gives it one),
   so a flex column here lets everything above keep its natural size and
   hands the remainder to the editor. */
.meeting-detail {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
.meeting-detail .meeting-content-row {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}
.meeting-detail .meeting-content-row .rich-editor {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.meeting-detail .meeting-content-row .rich-editable {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
/* FR#429: a meeting reads like a note. Same type, same line height, no
   boxed border — it is the same kind of writing, and the two looking
   different made moving between them feel like two different apps. */
.meeting-detail .meeting-content-row .rich-editable {
  font-size: calc(0.92rem * var(--note-font-scale, 1));
  line-height: 1.6;
  border: none;
  padding: 0.4rem 0.1rem;
}

/* Asked for: the formatting tools stay with the writing area as it
   scrolls. The editable is the scroll container here, so the toolbar
   sticks to the top of the editor around it rather than to the pane. */
.meeting-detail .meeting-content-row .rich-toolbar {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg);
  padding: 0.35rem 0;
}
:root[data-theme="light"] .meeting-detail .meeting-content-row .rich-toolbar { background: var(--card-bg); }

/* Asked for: the kind dropdown lines up with the date and time next to it.
   A <select> and an <input type=date> pick different intrinsic heights
   from the same padding, so it is stated outright. */
/* Not the "Gehad" checkbox: a checkbox stretched to the full width of its
   slot is not a field, and this rule is about date/time/select agreeing on
   a height. */
.meeting-meta-row .form-row input:not([type="checkbox"]),
.meeting-meta-row .form-row select {
  height: var(--meeting-field-h);
  width: 100%;
  /* A date input reports a wide intrinsic minimum, which a flex item
     honours unless told not to — that alone pushed the three fields onto
     separate lines and stole the height from the writing area below. */
  min-width: 0;
}
.meeting-detail-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-bottom: 0.3rem;
}

/* FR#372: the tick that closes a wish off. Aligned to the first line of
   the title rather than centred, since a wish with notes and a screenshot
   can be several lines tall. */
.feature-check {
  flex: 0 0 auto;
  align-self: flex-start;
  margin-top: 0.15rem;
  width: 1.05rem;
  height: 1.05rem;
  cursor: pointer;
}
.feature-item[data-status="done"] .feature-title { text-decoration: line-through; opacity: 0.65; }

/* FR#342/#343: a picture attached to a wish. Capped rather than natural
   size — a screenshot is usually taller than the card it sits in. */
.feature-image {
  display: block;
  max-width: 100%;
  max-height: 220px;
  object-fit: cover;
  border-radius: 8px;
  margin-top: 0.5rem;
}
.feature-image-current { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; }
.feature-image-current img { max-width: 160px; max-height: 120px; object-fit: cover; border-radius: 6px; }

/* FR#322: opened as a standalone Notes or To do's app — no back arrow, no
   bottom nav. Everything else (the note list, the board, search) is
   unchanged; this only removes the ways out of the page, which is what
   makes it read as its own app rather than a page inside a bigger one. */
:root[data-solo] .bottom-nav,
:root[data-solo] .topbar-project-menu { display: none !important; }
/* FR#377: the back arrow is hidden in a standalone app — there is nowhere
   to go back to — except while a note is open, where it is the way back to
   the list. go-back already closes an open note before anything else, so
   only its visibility needed changing. */
:root[data-solo]:not(:has(.notes-master-detail.mobile-detail-open)) .topbar-back-btn { display: none !important; }
/* The bottom padding existed to clear the nav that is no longer there. */
:root[data-solo] .app { padding-bottom: 0; }

/* Gevraagd: de naam van degene voor wie de to-do is, vóór de tekst. Een
   chip in dezelfde kleur als de accenten, zodat een rij in één oogopslag
   leest als "Sam · doe de was" zonder dat de naam voor tekst doorgaat. */
.todo-assignee-naam {
  display: inline-block;
  margin-right: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 0.78em;
  font-weight: 700;
  line-height: 1.35;
  vertical-align: baseline;
  white-space: nowrap;
}
/* Je eigen to-do's rustiger: op een bord dat vooral van jou is, zou elke
   regel anders dezelfde felle chip krijgen. */
.todo-assignee-naam.mine { background: var(--accent-tint); color: var(--accent-dark); }

.attribution-initial {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 17px;
  height: 17px;
  margin-left: 0.4rem;
  border-radius: 999px;
  background: var(--border);
  color: var(--muted);
  font-size: 0.65rem;
  font-weight: 700;
  vertical-align: middle;
  flex-shrink: 0;
}

/* FR#152: Home's inbox — unread rows carry a left accent bar and slightly
   stronger text, so "new since you last looked" is visible at a glance
   without the row changing size and shifting the list around when read. */
.inbox-unread-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  margin-left: 0.4rem;
  border-radius: 999px;
  background: #ef4444;
  color: #fff;
  font-size: 0.72rem;
  font-weight: 700;
  vertical-align: middle;
}
.home-activity-row.unread { border-left: 3px solid var(--accent); padding-left: 0.5rem; }
.home-activity-row.unread .home-activity-text > div:first-child { font-weight: 600; color: var(--text); }

/* FR#151: the note's author, inline in its controls row (see
   noteAuthorHtml) rather than on a line of its own. margin-right:auto
   pins it left so the project picker and ☰ stay grouped on the right. */
.note-author { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.75rem; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; }
/* Narrow panes (the split view on a laptop, or a phone) can't fit a name
   next to the formatting buttons — the initial chip alone still says who
   wrote it, and it carries the full name as its tooltip. */
@media (max-width: 1100px) {
  .rich-toolbar .note-author .note-author-name { display: none; }
}
/* Pushes the note's own controls to the far end of the formatting toolbar,
   so they read as a separate group from the formatting buttons rather than
   another button in the row. */
.note-toolbar-spacer { flex: 1 1 auto; min-width: 0.5rem; }

/* The Notes page heading duplicates the topbar page title on mobile (which
   only appears there — see .topbar-page-title), so it's dropped at that
   width and kept on desktop, where the topbar shows the app name instead
   and this is the only thing naming the page. */
@media (max-width: 859.98px) {
  .notes-page-heading { display: none; }
}
/* A section-title whose heading is gone (the project Notes tab, where the
   section tab strip already names it) shouldn't still reserve a full row's
   worth of left-hand space. */
.section-title:not(:has(h2)) { justify-content: flex-end; }


@media (max-width: 859.98px) {
  /* One pane at a time on a phone, so only one set is about what's on
     screen. Reading a note: the page's actions go away — you go back to
     the list for them. */
  .notes-page-header-actions.mobile-note-open .notes-page-search-input,
  .notes-page-header-actions.mobile-note-open [data-action="new-idea"],
  .notes-page-header-actions.mobile-note-open [data-action="toggle-notes-deleted"] { display: none; }
}

/* With the heading hidden on mobile, the actions row is the whole row —
   let it use the full width so the search field starts at the left edge
   instead of floating in from the middle. */
@media (max-width: 859.98px) {
  .notes-page-header-actions { flex: 1 1 auto; }
}

/* Offline / sync indicator. Amber while disconnected (changes are being
   held on this device), neutral while draining the queue — it's only in
   the topbar at all when there's something to report. */
.offline-badge {
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  background: #f59e0b;
  color: #fff;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
}
.offline-badge.syncing { background: var(--border); color: var(--muted); }

/* FR#296 follow-up: on a phone the topbar's back arrow already goes back a
   page, so the project header's own "← Back to projects" is a second way
   to do the same thing, one row further down. Desktop has no topbar arrow,
   so it keeps the button. */
@media (max-width: 859.98px) {
  .project-detail-root > button.ghost[data-action="go-projects"],
  .project-detail-root > button.ghost[data-action="go-calendar"] { display: none; }
}

/* ---------- Mobile chrome trims ---------- */
@media (max-width: 859.98px) {
  /* Every page's own leading heading repeats what the topbar already says
     at this width (see updateTopbarPageTitle), so it's two lines of the
     same word above the actual content. Sub-headings further down the page
     are untouched — they're the ones carrying real information. */
  .page-heading { display: none; }
  /* A section-title left holding only its buttons shouldn't still reserve
     the space the heading used to occupy. */
  .section-title:not(:has(h2:not(.page-heading))) { justify-content: flex-end; }

  /* Bigger tap targets along the bottom — this is the app's main
     navigation on a phone and was sized for a desktop pointer. */
  .bottom-nav { padding: 0.55rem 0.2rem calc(0.55rem + var(--safe-bottom)); }
  .bottom-nav .nav-btn { font-size: 1.45rem; gap: 0.15rem; padding-top: 0.35rem; padding-bottom: 0.35rem; }
  .bottom-nav .nav-btn span { font-size: 0.68rem; }
  .bottom-nav .nav-icon svg { width: 1.5em; height: 1.5em; }

  /* Home's recent notes are a plain list on a phone (not the desktop grid
     of tiles), and a boxed border around each row turned that list into a
     stack of competing rectangles. The row separator already does the job. */
  .note-carousel-item .note-grid-card { border: none; }
}

/* Find-in-note bar (Cmd/Ctrl+F with a note open). Sits above the note's
   text, outside .rich-editable so it can never be saved into the note. */
.note-find {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-bottom: 0.5rem;
  padding: 0.3rem 0.4rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.note-find-input {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  background: none;
  font-size: 0.9rem;
  padding: 0.2rem 0.1rem;
}
.note-find-input:focus { outline: none; }
.note-find-count { font-size: 0.75rem; flex-shrink: 0; }
/* Painted over Ranges rather than wrapped around them, so the note's own
   markup is never touched. */
::highlight(note-find) { background: #fde68a; color: #111; }

/* The feature-feed address: full width and selectable, since the only
   thing you do with it is copy it. */
.feature-feed-url {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
}

/* FR#306 hid the note's own ☰ on phones, because it was then the second
   identical hamburger on screen. FR#310 gave the section picker a chevron
   instead, so there's no longer a clash — and hiding it left a phone with
   no way at all to pin, move or delete the note you're reading without
   going back to the list first. It stays. */

/* The list pane scrolls, so its scrollbar sits hard against this row —
   give the controls a little room rather than letting them touch it. */
/* The list pane scrolls, so its scrollbar sits hard against this row —
   give the controls a little room rather than letting them touch it. */
.notes-list-toolbar { padding-right: 0.6rem; }
/* Asked for on Meetings specifically: more room again, so the last icon is
   not almost touching the divider you drag the pane by. Scoped there
   rather than shared, since Notes and Bonnetjes were not asked about and
   look right as they are. */
.meetings-master-detail .notes-list-toolbar { padding-right: 1.1rem; }

/* Phone: a button's label should shrink rather than break apart. Long
   Dutch labels ("+ Financiering toevoegen", "Niet opbergen — later
   beslissen") were wrapping mid-word into three lines. nowrap stops the
   break; the smaller size and min-width:0 are what let them still fit.
   The bottom nav and the pomodoro mode buttons are excluded — those stack
   an icon over a label by design, which is not the same thing. */
@media (max-width: 859.98px) {
  button.small,
  button.ghost,
  button.chip,
  .chip,
  .subtabs-mobile-trigger,
  .view-toggle button {
    /* Never split a word: "Financiering" must not become "Financie-⏎ring". */
    word-break: keep-all;
    overflow-wrap: normal;
    hyphens: none;
    min-width: 0;
    /* FR#346: one line, always. This used to allow a wrap at a space rather
       than clip, on the grounds that a clipped label is worse than a tall
       one — but a two-line button is half again as tall as its neighbours
       and drags the whole row down with it, which turned out to be the
       worse of the two on a phone. Ellipsis is the safety valve for a label
       long enough that shrinking can't save it; the labels that actually
       hit this have been shortened instead (FR#325, FR#349). */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    font-size: 0.75rem;
  }
}

/* The empty save-status reserved 3.5rem even with nothing in it, which on a
   375px phone pushed the note's ☰ clean off the right edge of the toolbar.
   It can size to its own text instead; the width it needs only exists while
   a save is actually in flight. */
@media (max-width: 859.98px) {
  .note-save-status { min-width: 0; flex: 0 1 auto; overflow: hidden; }
}

/* The formatting bar stays put while a long note scrolls under it, rather
   than scrolling away and leaving you to scroll back up to reach it. The
   detail pane is the scroll container (see the notes layout above), so
   sticky is relative to that. Opaque, or the text would show through. */
.note-fullscreen .rich-editor { position: relative; }
.note-fullscreen .rich-toolbar {
  position: sticky;
  top: 0;
  z-index: 3;
  /* De kleur van het vel waar de balk op ligt, niet die van de pagina
     eromheen. Dit stond alleen goed onder een uitdrukkelijk gekozen licht
     thema; koos je niets (de gewone stand) of donker, dan viel hij terug op
     --bg en lag er een grijze baan boven de tekst. */
  background: var(--card-bg);
  /* The top of this padding is the space .note-fullscreen used to have —
     see the note there. It also means that once stuck, the bar's own
     background covers that strip, so text never shows through above it. */
  padding: 0.7rem 0 0.35rem;
}

/* A note appearing in or leaving the list gets a short slide, so a new note
   is visibly added rather than the list silently reflowing under you. Only
   the row that actually changed animates: the class is applied by app.js to
   that id alone (see markNoteRowAnimation), not to the whole list on every
   render, which would make ordinary navigation twitch. */
@keyframes note-row-in {
  from { opacity: 0; transform: translateX(-14px); max-height: 0; }
  to { opacity: 1; transform: translateX(0); max-height: 12rem; }
}
@keyframes note-row-out {
  from { opacity: 1; transform: translateX(0); max-height: 12rem; }
  to { opacity: 0; transform: translateX(-14px); max-height: 0; }
}
.note-list-row.row-appear { animation: note-row-in 220ms ease-out; }
.note-list-row.row-leaving,
.meeting-list-row.row-leaving,
.todo-item.row-leaving { animation: note-row-out 200ms ease-in forwards; pointer-events: none; }

/* Bezig met aanmaken of opslaan: het formulier of de knop laat dat zien, en
   een tweede druk wordt genegeerd (zie eenmaalTegelijk in app.js). */
.todo-quick-add.is-bezig, form.is-bezig { opacity: 0.6; }
[data-action].is-bezig { opacity: 0.5; pointer-events: none; }

/* The homepage's project grid differs by viewport on purpose: recents on a
   desktop, favourites on a phone (see viewHome). Both are in the markup and
   CSS picks one, so resizing across the breakpoint needs no re-render. */
.home-projects-desktop, .home-projects-heading-desktop { display: none; }
.home-projects-phone, .home-projects-heading-phone { display: block; }
.home-projects-heading-phone { display: inline; }
@media (min-width: 860px) {
  .home-projects-desktop { display: block; }
  .home-projects-heading-desktop { display: inline; }
  .home-projects-phone, .home-projects-heading-phone { display: none; }
}

/* Asked for: on a phone the empty to-do list should be no taller than the
   Agenda list under it. The 200px minimum exists to make "click anywhere to
   add" a generous target on a desktop, where there is room to spare; on a
   phone it just pushed the Agenda most of a screen down for no gain. */
@media (max-width: 859.98px) {
  .home-mobile-panels .master-todo-list { min-height: 0; }
}

/* Asked for: with the keyboard up, the formatting tools disappeared and
   the back arrow could only be reached by scrolling. Both were pinned to
   the layout viewport, which iOS leaves alone when the keyboard shrinks the
   visible area — so both slid out of view. mountVisualViewportPinning in
   app.js publishes where the visible area actually is; these pin to that.

   Only while a keyboard is genuinely open, and only on phones, so nothing
   about the ordinary layout changes. */
@media (max-width: 859.98px) {
  /* No repositioning while the keyboard is up — deliberately.
     Pinning the bars to the visual viewport was meant to keep them on
     screen, and it did the opposite. Safari zooms the page when you focus
     text in a tab (maximum-scale is honoured only in an installed app), so
     a position:fixed bar spans the *zoomed* layout viewport — wider than
     what you can see — and both its ends fall off the screen. Measured on
     the device: correct with the keyboard down, clipped and shifted left
     the moment it came up.

     Ordinary sticky positioning already keeps the toolbar at the top of
     the note and the top bar at the top of the page, at the right width,
     under zoom as much as without it. What remains here is only what the
     keyboard genuinely changes: the room it takes up, and a nav that has
     no business floating over it. */

  /* Asked for: the bottom nav floated above the keyboard while typing. It
     is fixed to the bottom and iOS lifts fixed elements clear of the keys,
     so the one control you are not using sat between your text and the
     keyboard. It goes away while typing and comes back afterwards, which
     also gives the note the space back. */
  :root[data-keyboard="open"] .bottom-nav { display: none; }
  :root[data-keyboard="open"] .app { padding-bottom: 0; }

  /* Asked for: the last lines of a note could not be scrolled out from
     behind the keyboard. The scroll container ends where the layout
     viewport ends, and the keyboard covers the bottom third of that — so
     the text was there, and the scroll simply ran out before it could be
     lifted clear. This adds exactly the keyboard's own height as scrollable
     room at the end, so the final line can be brought up to where you can
     see it. It costs nothing when no keyboard is up: the inset is 0 and
     these rules don't apply at all. */
  /* On the editable, not the form around it: .note-fullscreen is height:100%
     with border-box, so padding there sits inside the height and adds no
     scrollable extent at all. The editable is what grows with the text, so
     it is what can carry the extra room. */
  :root[data-keyboard="open"] .note-fullscreen .rich-editable,
  :root[data-keyboard="open"] .meeting-detail .meeting-content-row .rich-editable {
    padding-bottom: calc(var(--vv-bottom-inset, 0px) + 2rem);
  }

  /* Asked for: the formatting bar was nowhere to be seen while typing. It
     sticks to the TOP of the note, which is right when you are reading —
     but the moment you are writing, the top of the note is usually far
     above the caret, and a bar you have to scroll back up to reach may as
     well not be there. While the keyboard is up it moves to the bottom
     instead, right above the keys, where a formatting bar belongs.

     Sticky rather than fixed, deliberately: a fixed bar spans the zoomed
     layout viewport and falls off both edges of the screen, which is the
     bug the block above this one exists to avoid. Sticky is measured
     against the scroll container, which already ends above the keyboard. */
  /* FR#448 liet de balk los zodra je alleen aan het lezen was: op een
     telefoon is het scherm vrijwel helemaal notitie, en een rij knoppen die
     daar een hele scroll lang overheen ligt is veel.
     Gevraagd, daarna: de knoppen moeten wél meekomen als je scrolt —
     anders moet je helemaal terug naar boven om één woord vet te maken.
     Dus weer vastgezet aan de bovenkant; zodra je typt verhuist hij naar
     onderen, vlak boven het toetsenbord (de regel hieronder). */
  /* Gemeld: de knoppen scrollen nog steeds niet mee.
     Sticky bleek daar niet te vertrouwen — op iOS laat hij los zodra de
     pagina zelf meeschuift of het toetsenbord de boel verschuift. Dus niet
     meer "mee laten schuiven en hopen dat hij blijft plakken", maar de balk
     helemaal buiten het scrollende deel zetten: de notitie wordt een kolom
     met een vaste balk en een tekstvlak dat als enige scrolt. Dan kán hij
     niet meer weglopen.

     De pen (het tekstvlak) is nu zelf de scrollcontainer; de notitiepagina
     eromheen scrollt niet meer. */
  .notes-master-detail.mobile-detail-open .notes-detail-pane { overflow: hidden; }
  .note-fullscreen { display: flex; flex-direction: column; min-height: 0; }
  .note-fullscreen .rich-editor {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1 1 auto;
  }
  .note-fullscreen .rich-toolbar {
    position: static;
    z-index: 3;
    flex: 0 0 auto;
    background: var(--card-bg);
  }
  .note-fullscreen .rich-editable {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* Tijdens het typen hoort de balk onderaan, vlak boven de toetsen. Met
     deze kolom is dat simpelweg een andere volgorde: de balk ná het
     tekstvlak in plaats van ervoor. Geen sticky, geen fixed — dus ook geen
     balk die halverwege losschiet. */
  :root[data-keyboard="open"] .note-fullscreen .rich-editor { flex-direction: column; }
  :root[data-keyboard="open"] .note-fullscreen .rich-toolbar {
    order: 2;
    margin: 0;
    padding: 0.35rem 0 calc(0.35rem + var(--vv-bottom-inset, 0px));
    border-top: 1px solid var(--border);
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12);
  }
  :root[data-keyboard="open"] .note-fullscreen .rich-editable { order: 1; }
}

/* The off-screen input used to open the keyboard inside a tap (see
   primeKeyboard). It has to be focusable, so it cannot be display:none or
   visibility:hidden — it is simply too small and too far away to see. */
#kb-primer {
  position: fixed;
  top: 0;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  border: none;
  padding: 0;
  font-size: 16px; /* under 16 and iOS zooms the page on focus */
}

/* The keyboard-state readout (?kbdebug=1). Deliberately loud and out of
   the way of the top bar it is reporting on. */
#kb-debug {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  background: #111;
  color: #0f0;
  font: 11px/1.5 ui-monospace, Menlo, monospace;
  padding: 4px 6px;
  text-align: center;
  pointer-events: none;
}

/* The formatting bar wraps on a phone rather than running off the edge.
   Eight buttons laid out nowrap are wider than a 375px screen, so the ends
   were simply cut off — "Aa" gone from the left, the note menu from the
   right. This is a question of width, not of keyboards: it applies whenever
   the screen is narrow, whether or not anything is being typed. A second
   row costs a little height and hides nothing. */
@media (max-width: 859.98px) {
  .rich-toolbar {
    flex-wrap: wrap;
    row-gap: 0.3rem;
  }
  .rich-toolbar button {
    min-width: 28px;
    padding: 0.3rem 0.32rem;
  }
}

/* FR#380: a phone scrolls up and down only. Nothing here is meant to move
   sideways as a whole page — the few things that scroll horizontally (the
   section picker, a wide table) are their own containers and keep doing so.
   Without this, an overshoot inside one of those, or any element that
   momentarily measures too wide, drags the entire window across and leaves
   it there. */
@media (max-width: 859.98px) {
  html, body {
    /* clip, not hidden. overflow-x:hidden makes the element a scroll
       container, and a sticky element sticks to its nearest scrolling
       ancestor — so with this on body the top bar was sticking to a
       scrollport that itself scrolled away, taking the back arrow with it.
       clip does the same job without creating one. */
    overflow-x: clip;
    max-width: 100%;
  }
}

/* Asked for: the notes page opened with a dead band above the search row.
   On a phone the page's own heading is hidden (the top bar shows it
   instead), so its .section-title is an empty element contributing only
   its margin — and the view's top padding sat on top of that. The list is
   the whole point of the page, so it starts near the top. */
@media (max-width: 859.98px) {
  .view-root:has(> .notes-master-detail) { padding-top: 0.3rem; }
  .section-title:has(+ .notes-master-detail) { margin-bottom: 0; }
}

/* FR#396: the time-based to-do board — days inside "Deze week", and
   to-dos grouped under their project inside each day. */
.todo-day { margin-bottom: 1.9rem; }
.todo-day-heading {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  /* Gevraagd: meer lucht boven en onder de naam van de dag, zodat de dagen
     uit elkaar vallen in plaats van als één lange lijst te lezen. */
  padding: 0.9rem 0 0.45rem;
  margin-bottom: 0.55rem;
}
/* Behalve bij de eerste dag: daar is de ruimte boven al de ruimte onder de
   kop van de pagina. */
.todo-col > .todo-day:first-child > .todo-day-heading { padding-top: 0.15rem; }
@media (hover: hover) {
  .todo-day-heading .todo-col-add { opacity: 0; transition: opacity 120ms ease; }
  .todo-day-heading:hover .todo-col-add,
  .todo-day-heading .todo-col-add:focus-visible { opacity: 1; }
}

/* FR#406: a day folds shut. The chevron turns, the count stays visible so
   a folded day still says how much is in it. */
.todo-day-heading { cursor: pointer; user-select: none; }
.todo-day-label { display: inline-flex; align-items: center; gap: 0.3rem; }
/* Gemeld: het pijltje van de dag stond niet op één lijn met dat van de
   projecten. Het had een eigen maat en een eigen breedte, dus kwam het
   midden ervan net ergens anders uit. Nu hetzelfde blokje als
   .todo-project-klap: zelfde breedte, zelfde lettergrootte, gecentreerd. */
.todo-day-chevron {
  display: inline-block;
  flex: none;
  width: 1.1rem;
  font-size: 0.7rem;
  line-height: 1;
  text-align: center;
  transition: transform 140ms ease;
}
.todo-day.is-collapsed .todo-day-chevron { transform: rotate(-90deg); }
/* Asked for: the fold rolls rather than snapping. grid-template-rows is
   what makes this animate from real content height without measuring it in
   JS — 0fr to 1fr is interpolatable where height:auto is not.
   It needs exactly ONE grid child, which is what .todo-day-body-inner is
   for: with several children the extra ones land in implicit auto-sized
   rows, so setting the first to 0fr collapsed only that first row and left
   everything below it standing at full height. */
.todo-day-body {
  display: grid;
  grid-template-rows: 1fr;
  /* A grid item's default min-width is auto, meaning "never narrower than
     my content" — and the fold animation above made this a grid. So one
     un-wrappable row (an appointment title, which is nowrap) set a floor
     for the whole day, the day grew wider than the screen, and every
     to-do under it then laid out against that wider box and ran off the
     right edge. That is what "the list doesn't fit" was: not the to-dos,
     but the grid refusing to shrink around them. */
  min-width: 0;
  transition: grid-template-rows 220ms ease, opacity 180ms ease;
}
.todo-day-body-inner { min-height: 0; min-width: 0; }
.todo-appointments, .todo-project-group { min-width: 0; }
.todo-day.is-collapsed .todo-day-body {
  grid-template-rows: 0fr;
  opacity: 0;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .todo-day-body { transition: none; }
}
.todo-day-count {
  font-weight: 600;
  font-size: 0.9em;
  opacity: 0.75;
}

/* Today reads as the one that matters; the rest of the week is context. */
/* Gevraagd: "Vandaag" hoeft niet op te vallen met een eigen kleur; hij
   staat toch altijd bovenaan. Zelfde grijs als de andere dagen. */

/* Asked for: the same space under every day. Three different bottom
   margins used to apply here — 1.4rem between groups, 2rem after the
   unfiled one, 0.2rem for the last — so how much air sat above a day's
   rule depended on what that day happened to contain. One value between
   groups and none after the last; the day's own margin is then the only
   thing below it, and it is the same for every day. */
.todo-project-group { margin-bottom: 1.2rem; }
.todo-project-group:last-child { margin-bottom: 0; }
/* Same type as the to-dos underneath it — same size, same colour — and
   bold is the only thing that sets it apart. It's a label for the list,
   not a different kind of text. */
.todo-project-heading {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 0.35rem;
  /* Gevraagd: de projectnaam staat recht onder de datum, tegen dezelfde
     linkerkant aan — geen inspringing hier, die zit bij de to-do's zelf. */
  padding-left: 0;
  cursor: grab;
  /* Gevraagd: op de kop drukken klapt in en uit; dan hoort er geen tekst
     blauw te worden omdat je net iets te snel twee keer drukt. Net als bij
     de kop van een dag. */
  user-select: none;
  -webkit-user-select: none;
}
/* Gevraagd: boven de streep die van de naam naar het pijltje loopt een
   gewone cursor. Die streep is lege ruimte om de kop op te rekken; een
   handje of een greep-cursor belooft daar iets wat er niet is. */
.todo-heading-rule { flex: 1; cursor: default; }
.todo-project-name { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; }
/* The "+" belongs to the heading it's on: it adds to that project, on that
   day. Waits for the pointer, like the bin does — on touch there is no
   hover, so it stays. */
.todo-project-add {
  flex: none;
  background: none;
  border: none;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.35rem;
  color: var(--muted);
  cursor: pointer;
}
.todo-project-add:hover { color: var(--accent-dark); }

/* Gevraagd: het klappijltje staat aan de rechterkant van het project (en
   van de agenda). Anders dan de "+" blijft het altijd staan — het zegt ook
   wat de stand is, en dat mag je niet hoeven ontdekken door met de muis
   langs te gaan. */
.todo-project-klap {
  flex: none;
  background: none;
  border: none;
  width: 1.1rem;
  font-size: 0.7rem;
  line-height: 1;
  padding: 0;
  text-align: center;
  color: var(--muted);
  cursor: pointer;
}
.todo-project-klap:hover { color: var(--accent-dark); }
/* Gevraagd: wat meer lucht tussen de kop "To do's" en de eerste dag. */
#all-todos-board { margin-top: 1.1rem; }

/* Gevraagd: op de telefoon een zwevend knopje rechtsonder voor een nieuwe
   to-do. Boven de balk onderin, zodat het die niet overlapt. Op een groot
   scherm staat de "+" bij elke dag al in beeld en is dit alleen in de weg. */
.todo-zwevende-plus { display: none; }
@media (max-width: 859.98px) {
  .todo-zwevende-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    right: 1rem;
    bottom: calc(4.4rem + var(--safe-bottom));
    width: 52px;
    height: 52px;
    border: none;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    z-index: 19;
  }
  .todo-zwevende-plus:active { transform: scale(0.94); }
  .todo-zwevende-plus .nav-icon svg { width: 22px; height: 22px; }
}

/* Gevraagd (met een schets erbij): alle kopjes beginnen tegen de linkerrand
   — de dag, de projecten en de agenda op dezelfde streep — en wat eronder
   hangt springt in. Zo zie je in één oogopslag waar een kop staat en wat
   erbij hoort. */
/* Gevraagd: het rondje van een to-do begint precies waar de eerste letter
   van de projectnaam erboven begint.
   De kop is [vinkje][ruimte][naam]; een to-do-rij heeft zelf al wat ruimte
   en een sleepgreep vóór zijn rondje. De inspringing hieronder is het
   verschil tussen die twee, zodat de twee rondjes niet onder elkaar maar
   het rondje onder de létter uitkomt. */
.todo-col { --todo-vink-breedte: 25px; }
.todo-project-body { padding-left: 0; }

/* Het vinkje op de kop van een project of van de agenda (gevraagd): alles
   eronder in één keer af, en het staat vanzelf aan als alles al af is. */
.todo-project-check {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--todo-vink-breedte);
  cursor: pointer;
}
/* Gevraagd: precies hetzelfde vinkje als bij een to-do — zelfde rand,
   zelfde maat, zelfde vorm. Vandaar dat het die klasse gewoon meekrijgt en
   hier alleen staat wat afwijkt. */
.todo-project-check input:disabled { opacity: 0.35; cursor: default; }

/* In- en uitklappen met een beweging (gevraagd): de hoogte loopt netjes
   dicht in plaats van in één beeld te verspringen. Het getal waar hij
   naartoe beweegt wordt gemeten in klapMetBeweging(); hier staat alleen
   hoe snel, en waar hij uitkomt. */
.todo-project-group > .todo-project-body {
  overflow: hidden;
  transition: height 180ms ease, opacity 180ms ease;
}
.todo-project-group.is-dicht > .todo-project-body {
  height: 0;
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .todo-project-group > .todo-project-body { transition: none; }
}
.todo-project-group.is-dicht .todo-project-heading { margin-bottom: 0.6rem; }

/* De agenda als eigen kopje op de algemene lijst (gevraagd): dezelfde vorm
   als een project, zodat de dag één rij kopjes is. */
/* Gevraagd: een handje boven "Agenda" — je kunt erop drukken om hem in en
   uit te klappen. Geldt net zo voor "Algemeen": die twee kun je niet
   slepen (er valt geen volgorde aan te wijzigen), dus daar is de greep-
   cursor van een project niet de juiste. */
.todo-project-heading:not([data-drag-handle]) { cursor: pointer; }
@media (hover: hover) {
  .todo-project-add { opacity: 0; transition: opacity 120ms ease; }
  .todo-project-heading:hover .todo-project-add,
  .todo-project-add:focus-visible { opacity: 1; }
}

/* The checkbox that replaced dragging a card into a "done" column.
   Round, asked for. */
.todo-check {
  flex: none;
  width: 17px;
  height: 17px;
  margin: 0.15rem 0.1rem 0 0;
  appearance: none;
  -webkit-appearance: none;
  border: 1.5px solid var(--checkbox-border);
  border-radius: 50%;
  background: var(--card-bg);
  cursor: pointer;
  position: relative;
}
.todo-check:checked { background: var(--accent); border-color: var(--accent); }
.todo-check:checked::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 1px;
  width: 4px;
  height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

@media (hover: hover) {
  .todo-item .danger-icon { opacity: 0; transition: opacity 120ms ease; }
  .todo-item:hover .danger-icon,
  .todo-item .danger-icon:focus-visible { opacity: 1; }
}

/* Clicking anywhere else on the row ticks it off — see .todo-item
   .todo-title above for the caret that marks out the exception. */
.todo-content { cursor: default; }

/* Ticked to-dos stay where they are, struck through (asked for) rather
   than vanishing or moving — you can see what you got done today. */
.todo-item.done .todo-title { text-decoration: line-through; opacity: 0.55; }

/* FR#361: the "Meer" page — everything that isn't one of the four main
   destinations, laid out as tiles. Sized so a row of them reads as a set of
   apps rather than a list of links, and so each is a comfortable target. */
.more-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 0.7rem;
  margin-bottom: 0.5rem;
}
.more-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  padding: 0.9rem 0.4rem;
  min-height: 92px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  cursor: pointer;
  color: var(--text);
  font: inherit;
  text-align: center;
  transition: opacity 0.15s ease;
}
.more-tile:active { opacity: 0.5; transition: none; }
.more-tile-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 12px;
  background: var(--accent-tint);
  color: var(--accent-dark);
}
.more-tile-icon svg { width: 22px; height: 22px; }
.more-tile-label {
  font-size: 0.78rem;
  line-height: 1.2;
  word-break: keep-all;
  overflow-wrap: normal;
}
/* Desktop reaches all of this from the sidebar, so the page is only ever a
   phone thing — but it stays reachable by URL rather than being blocked. */
@media (min-width: 860px) {
  .more-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); max-width: 640px; }
}

/* FR#328/#364: swipe a note row to uncover a delete button.
 *
 * The row holds still and its contents slide, so the list's own scrolling
 * geometry is untouched — moving the row would give the pane a horizontal
 * scrollbar.
 *
 * Swipe LEFT, button on the RIGHT, which is the direction every list on a
 * phone uses for this. The button is invisible until a swipe starts: it is
 * absolutely positioned and later in the DOM than the text, so it paints
 * *above* it, and left visible it sat permanently over the right-hand end
 * of every row. Fading it in with the gesture also means it appears
 * exactly when the space for it does.
 */
.note-list-row { position: relative; overflow: hidden; }
.note-list-row > div {
  transform: translateX(calc(-1 * var(--swipe-x, 0px)));
  transition: transform 0.18s ease;
}
/* No transition while the finger is down, or the row lags behind it. */
.note-list-row.swiping > div { transition: none; }
.note-list-row.swiped { --swipe-x: 76px; }
.note-swipe-delete {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: #dc2626;
  color: #fff;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.note-list-row.swiping .note-swipe-delete { opacity: 1; }
.note-list-row.swiped .note-swipe-delete { opacity: 1; pointer-events: auto; }
/* Touch widths only: a desktop row already has right-click, and a button
   hiding behind every row is something to reveal by accident with a
   trackpad. */
@media (min-width: 860px) {
  .note-swipe-delete { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .note-list-row > div, .note-swipe-delete { transition: none; }
}

/* The same gesture on a to-do. The card is a flex row of several children
   rather than one wrapper, so they all slide together — everything except
   the button they're sliding off. */
.todo-item { overflow: hidden; }
.todo-item > *:not(.todo-swipe-delete) {
  transform: translateX(calc(-1 * var(--swipe-x, 0px)));
  transition: transform 0.18s ease;
}
.todo-item.swiping > *:not(.todo-swipe-delete) { transition: none; }
.todo-item.swiped { --swipe-x: 76px; }
.todo-swipe-delete {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: #dc2626;
  color: #fff;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.todo-item.swiping .todo-swipe-delete { opacity: 1; }
.todo-item.swiped .todo-swipe-delete { opacity: 1; pointer-events: auto; }
/* Touch widths only, same as the note rows: a desktop card has right-click
   and a bin on hover already. And with the swipe there, the bin that used
   to sit on every card on a phone is one affordance too many. */
@media (min-width: 860px) {
  .todo-swipe-delete { display: none; }
}
@media (max-width: 859.98px) {
  .todo-item > .danger-icon { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .todo-item > *:not(.todo-swipe-delete), .todo-swipe-delete { transition: none; }
}

/* FR#355: a project card leaving. Scales down as well as fading — a grid
   cell that only faded left its space reserved until the re-render, which
   is the jump that made the delete feel unfinished. */
@keyframes project-card-out {
  to { opacity: 0; transform: scale(0.92); }
}
.project-card.card-leaving { animation: project-card-out 200ms ease-in forwards; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .project-card.card-leaving { animation-duration: 1ms; }
}
@media (prefers-reduced-motion: reduce) {
  .note-list-row.row-appear, .note-list-row.row-leaving,
  .meeting-list-row.row-leaving { animation-duration: 1ms; }
}

/* The gap after a checklist box is spacing, not content — so it lives here
   rather than as a space character inside the note's text. */

/* Inside a project on a phone, the section heading repeats what the picker
   directly below it already says ("Notities" above "Notities ▾"). The
   picker stays — it's the control — and the heading goes. Desktop keeps
   it: there the sections are a sidebar list, not a labelled dropdown. */
@media (max-width: 859.98px) {
  .project-layout .section-title h2 { display: none; }
  /* FR#332: .section-title is also the header of a card — a meeting's
     title, a day's heading in the time list — and those are content, not a
     repeat of the picker. Hiding them left a meeting card showing an edit
     icon, a bin and a date, with the title rendering 0x0: nothing to read
     and nothing to tap, since tapping the title is what expands the card. */
  .project-layout .card .section-title h2 { display: block; }
}

/* Project actions beside the project's name in the topbar. Phone only —
   on desktop the project page has its own header with this button, and
   the topbar there shows the app name rather than the page title. */
.topbar-project-menu { display: none; background: none; border: none; cursor: pointer; font-size: 1.1rem; color: var(--text); padding: 0 0.2rem; }
@media (max-width: 859.98px) {
  .topbar-project-menu:not(.hidden) { display: inline-flex; align-items: center; }
}

/* Home on a phone: five recent notes is a glance; the rest is scrolling
   past the sections below it. Matches the same cap already applied to
   Recente projecten. "Alle notities" remains the way to the full list. */
@media (max-width: 859.98px) {
  .notes-carousel .note-carousel-item:nth-child(n+6) { display: none; }
}

/* The project header stays on every section now, but a phone still doesn't
   want it: the topbar there already shows the project's name and its menu,
   so this would be the same information twice. The pieces were already
   hidden at this width; the menu button joins them. */
@media (max-width: 859.98px) {
  .detail-header-menu { display: none; }
}

/* Phone: a long project name is cut with an ellipsis rather than wrapping,
   so every card in the list stays the same height and the grid doesn't go
   ragged. The full name is still there on the project's own page. */
@media (max-width: 859.98px) {
  .project-card-content h3 {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .sidebar-favorite-btn span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ======================================================================
   CALENDAR (asked for, 2026-09-01) — the month grid on the Agenda page.
   ====================================================================== */
.cal-grid {
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  margin-bottom: 0.8rem;
  background: var(--border);
}
.cal-weekdays, .cal-week-cells, .cal-week-bars {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.cal-weekdays { gap: 1px; }
/* A week is a row of cells with a layer of event bars over it, so a
   multi-day event can be a single bar across several columns instead of
   its title repeated in each.
   The cells are the *background* and the bars are in normal flow: that way
   a busy day makes its week taller instead of spilling its events into the
   week below, which is what happened when the bars were the absolute
   layer. Clicks fall through the bar layer to the day underneath. */
.cal-week { position: relative; min-height: 5.5rem; }
.cal-week-cells { position: absolute; inset: 0; gap: 1px; }
.cal-week-bars {
  position: relative;
  gap: 1px 2px;
  padding: 1.6rem 2px 0.3rem;
  pointer-events: none;
}
.cal-week-bars .cal-chip { pointer-events: auto; }
.cal-weekday {
  background: var(--bg);
  padding: 0.35rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-align: center;
}
.cal-cell {
  background: var(--card-bg);
  padding: 0.25rem 0.3rem 0.35rem;
  cursor: pointer;
}
/* Days either side of this month stay visible for continuity, but recede —
   a grid that stops dead at the 1st is harder to read across a week. */
.cal-cell.is-outside { background: var(--bg); }
.cal-cell.is-outside .cal-daynum { opacity: 0.4; }
.cal-daynum { font-size: 0.75rem; color: var(--muted); }
.cal-cell.is-today .cal-daynum {
  color: #fff;
  background: var(--accent);
  border-radius: 999px;
  width: 1.4rem;
  height: 1.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

.cal-chip {
  /* The calendar's own colour is the whole point of the chip — which of
     the three it belongs to has to be readable at a glance. */
  border: none;
  border-left: 3px solid var(--cal-color);
  background: color-mix(in srgb, var(--cal-color) 12%, transparent);
  color: var(--text);
  border-radius: 4px;
  padding: 1px 4px;
  font-size: 0.72rem;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-chip:hover { background: color-mix(in srgb, var(--cal-color) 22%, transparent); }
/* Subscribed events are somebody else's: shown, but marked as not yours
   to edit. */
.cal-chip.is-external { border-left-style: dashed; }
/* A bar clipped by the edge of the week loses its cap on that side, so it
   reads as continuing rather than ending on a Sunday. */
.cal-chip.continues-left { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cal-chip.continues-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.cal-chip.is-span { background: color-mix(in srgb, var(--cal-color) 20%, transparent); }
/* Deadlines and meetings are shown here but live elsewhere — outlined
   rather than filled, so they don't read as appointments you can drag. */
.cal-chip.is-deadline, .cal-chip.is-meeting {
  background: transparent;
  border: 1px solid var(--cal-color);
  border-left-width: 3px;
}
.cal-chip-time { font-variant-numeric: tabular-nums; opacity: 0.75; }

.cal-nav { display: flex; align-items: center; gap: 0.3rem; }
.cal-month-label { font-size: 0.9rem; font-weight: 600; min-width: 9ch; text-align: center; }

.cal-legend { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.8rem; }
.cal-legend-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.cal-legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; cursor: pointer; min-width: 0; }
.cal-swatch { width: 0.7rem; height: 0.7rem; border-radius: 3px; flex: none; }
.cal-legend-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-legend-tag {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 0.35rem;
}

.cal-settings, .cal-event-form {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.7rem 0.8rem;
  margin-bottom: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.cal-settings h3 { font-size: 0.95rem; }
.cal-settings-label { font-size: 0.78rem; color: var(--muted); }
.cal-settings-row { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.cal-settings-row input[type="text"] { flex: 1 1 12rem; min-width: 0; }
.cal-event-fields { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.cal-event-fields label { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.78rem; color: var(--muted); flex: 1 1 8rem; min-width: 0; }

/* A month of 5.5rem cells doesn't fit a phone; the grid scrolls sideways
   rather than crushing seven columns into 375px. */
@media (max-width: 859.98px) {
  .cal-grid { min-width: 34rem; }
  .cal-grid-scroll { overflow-x: auto; }
  .cal-week { min-height: 4.2rem; }
}

/* The iCloud link, inside a calendar's Sync panel. */
.cal-connect { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.cal-connect input { flex: 1 1 12rem; min-width: 0; }
.cal-linked { font-size: 0.85rem; }

/* Apple-Calendar-style popover: the event form floats beside the day or
   the chip it belongs to, rather than under the grid where the day you
   just clicked has often scrolled out of view. */
.cal-popover {
  position: fixed;
  z-index: 60;
  width: min(22rem, calc(100vw - 1.5rem));
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
  background: var(--card-bg);
  border: 1px solid var(--border);
}
.cal-popover .cal-event-form { margin: 0; border: none; box-shadow: none; background: none; }
.cal-popover-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.cal-popover-head h3 { font-size: 0.95rem; }

/* Appointments shown above a day's to-dos on the board. Quieter than a
   to-do row and with no checkbox: they're context for the day, not work
   you tick off. */
.todo-appointments { display: flex; flex-direction: column; gap: 1px; margin: 0 0 0.4rem; }
.todo-appointment {
  display: flex;
  gap: 0.45rem;
  align-items: center;
  min-width: 0;
  /* FR#417: same metrics as a to-do row, so the two read as one list of
     what the day holds — the colour dot is what says which is which. */
  padding: 0.35rem 0.5rem;
  font-size: 0.92rem;
  border-radius: 8px;
}
.todo-appointment .todo-check { accent-color: var(--cal-color); }
/* Gevraagd: is een afspraak afgevinkt, dan gaat de streep ook door de tijd
   — de tijd hoort bij dezelfde regel en bleef als enige overeind staan. */
.todo-appointment.done .todo-appointment-title,
.todo-appointment.done .todo-appointment-time { text-decoration: line-through; opacity: 0.55; }
.todo-appointment-title { cursor: text; }
.todo-appointment-time { cursor: text; }
/* Gevraagd: een afspraak die uit een agenda komt kun je hier niet
   hernoemen of verzetten. De tekstcursor beloofde dat wel — die zegt "hier
   kun je typen". Een verboden-teken was te luid voor iets wat gewoon een
   regel om te lezen is; nu de gewone pijl. Waarom het niet kan, staat bij
   het aanwijzen in de titel. */
.todo-appointment[data-gekoppeld="1"] .todo-appointment-title,
.todo-appointment[data-gekoppeld="1"] .todo-appointment-time { cursor: default; }
/* Asked for: editing an agenda item should look like editing a to-do —
   the same invisible field that fills the space the text was in, tinting
   only while focused. Three classes deep for the same reason
   .todo-title-input is (see that rule): the global input reset uses two
   :not() clauses and out-specificities a plain class selector, so a
   simpler rule here would silently keep the reset's border and padding. */
.todo-appointment .appt-time-input,
.todo-appointment .appt-title-input {
  font: inherit;
  font-size: 0.92rem;
  color: inherit;
  background: transparent;
  border: none;
  border-radius: 6px;
  margin: -0.2rem -0.3rem;
  padding: 0.2rem 0.3rem;
  min-width: 0;
}
.todo-appointment .appt-time-input:focus,
.todo-appointment .appt-title-input:focus {
  outline: none;
  border-color: transparent;
  box-shadow: none;
  background: var(--bg);
}
/* The time field only needs to be as wide as a time. */
.todo-appointment .appt-time-input { flex: 0 0 auto; width: 6.5rem; }
.todo-appointment .appt-title-input { flex: 1; }
/* Gevraagd: de tijd in dezelfde kleur als de regel zelf, niet in grijs. */
.todo-appointment-time { font-variant-numeric: tabular-nums; flex: none; font-size: 0.85rem; color: var(--text); }
/* min-width:0 is what makes the ellipsis work at all. A flex item will
   not shrink below its own min-content width, and for a nowrap line that
   IS the whole string — so this row could not narrow, the day's content
   box grew wider than the screen, and every to-do under it then laid out
   against that wider box and ran off the right edge. The clipped rows were
   never the problem; the appointment above them was. */
.todo-appointment-title {
  color: var(--text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cal-swatch-outline { background: transparent !important; border: 2px solid; }
.cal-settings-check { font-size: 0.85rem; gap: 0.45rem; }

/* FR#423: shown until a calendar is linked. A banner, not a modal — the
   grid behind it is still usable, and a dialog you must dismiss to look at
   an empty calendar helps nobody. */
.cal-setup-prompt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  padding: 0.7rem 0.9rem;
  margin-bottom: 0.8rem;
  border: 1px solid var(--accent);
  background: var(--accent-tint);
  border-radius: 10px;
}

/* ======================================================================
   MEETINGS (FR#415–421)
   ====================================================================== */
/* FR#421: two headed groups rather than striking finished ones through —
   a meeting that happened is a record, not a cancelled plan, and a list of
   struck-out text is hard to read at any length. */
.meeting-group-heading {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  padding: 0.6rem 0.3rem 0.3rem;
}
.meeting-group-count { font-weight: 600; opacity: 0.7; }
/* Asked for: air above "Afgeronde meetings". The two groups ran together,
   so the first finished meeting read as the last planned one. Only the
   second heading — the first sits at the top of the list already. */
.meeting-group-heading ~ .meeting-group-heading { margin-top: 1.6rem; }

/* FR#420: the kind of conversation, picked by its icon. */
/* The ☰ that now holds the bin, sitting at the end of the formatting bar. */
.rich-toolbar-menu { margin-left: auto; flex: 0 0 auto; }
.form-row-wide { flex: 1 1 100%; min-width: 0; }

/* FR#418: the writing area scrolls on its own, so the title, date and
   attendees stay put while you read down a long transcript — they are what
   tells you whose meeting this is, and losing them to a scroll was the
   thing that made a long one hard to follow. */
@media (min-width: 860px) {
  .meetings-master-detail .meeting-detail {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }
  .meetings-master-detail .meeting-detail > .meeting-content-row,
  .meetings-master-detail .meeting-content-row > .rich-editor {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .meetings-master-detail .meeting-detail .rich-editable {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }
}

/* FR#414: who else is in this note. A quiet line above the text — it is
   information about the room, not a thing to act on, until their version
   is actually waiting for you. */
/* Asked for: waar de ander typt. Een streepje in hun kleur met hun naam
   erboven, in een laag over de notitie (pointer-events uit, zodat klikken
   gewoon in de tekst terechtkomt). */
.has-remote-carets { position: relative; }
.remote-carets { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 3; }
.remote-caret {
  position: absolute; width: 2px; margin-left: -1px;
  background: var(--caret-kleur); border-radius: 1px;
  transition: top 90ms linear, left 90ms linear;
}
.remote-caret.is-typing { animation: remote-caret-pols 1s ease-in-out infinite; }
.remote-caret-naam {
  position: absolute; bottom: 100%; left: -1px; margin-bottom: 2px;
  padding: 1px 6px; border-radius: 6px 6px 6px 0;
  background: var(--caret-kleur); color: #fff;
  font-size: 0.68rem; font-weight: 600; line-height: 1.35; white-space: nowrap;
}
@keyframes remote-caret-pols { 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) {
  .remote-caret { transition: none; }
  .remote-caret.is-typing { animation: none; }
}

/* Gevraagd: kleiner, en op de regel met de andere knoppen in plaats van een
   eigen strook erboven. Neemt geen hoogte meer in als er niemand meekijkt. */
.note-presence {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.72rem;
  color: var(--muted);
  padding: 0;
  min-width: 0;
  max-width: 45%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.note-presence.hidden { display: none; }
.note-presence-typing { color: var(--accent-dark); }
.note-typing-dots { display: inline-flex; gap: 2px; margin-left: 3px; vertical-align: middle; }
.note-typing-dots i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  animation: note-typing 1.2s infinite ease-in-out;
}
.note-typing-dots i:nth-child(2) { animation-delay: 0.15s; }
.note-typing-dots i:nth-child(3) { animation-delay: 0.3s; }
@keyframes note-typing {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .note-typing-dots i { animation: none; opacity: 0.6; }
}

/* FR#427: a calendar's colour is what tells its events from the others' at
   a glance, so it is yours to pick. */
.cal-color-choices { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
.cal-color-swatch {
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}
.cal-color-swatch.active { border-color: var(--text); }
.cal-color-custom {
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  cursor: pointer;
}
/* Gevraagd: geen prullenbakje meer bij een afspraak. De knop blijft staan
   omdat Backspace en de rechtermuisknop hem indrukken — zo staat het
   verwijderen (met de vraag of je het zeker weet) op één plek — maar je
   ziet hem niet en je kunt er niet op klikken of tabben. */
.appt-delete.is-verborgen {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}
/* (De aparte regel voor aanraakschermen is vervallen: de knop is nu overal
   onzichtbaar, en display:none zou hem ook onbruikbaar maken voor het
   verwijderen dat Backspace en de rechtermuisknop erlangs sturen.) */

/* Asked for: a quick-add opened under a to-do should look like one that is
   already there — same row metrics, same checkbox position, so typing a
   run of them reads as writing a list rather than filling in a form. */
.todo-quick-add.is-inline {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  /* Exactly a to-do row's metrics: same padding, same gap, no box. It was
     a white rounded card sitting among transparent rows, which is what
     read as "rounded instead of square". */
  padding: 0.35rem 0.5rem;
  margin-bottom: 0.1rem;
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.todo-quick-add.is-inline select {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 0.8rem;
  padding: 0.1rem 0.3rem;
}
.todo-quick-add.is-inline .todo-check { pointer-events: none; opacity: 0.5; }
.todo-quick-add.is-inline .todo-drag-grip { opacity: 0.15; cursor: default; }
.todo-quick-add.is-inline input[name="title"] {
  flex: 1;
  min-width: 0;
  font-size: 0.92rem;
  border: none;
  border-radius: 0;
  background: none;
  padding: 0;
  margin: 0;
  outline: none;
  color: var(--text);
}
/* The date and project pickers that sit on the same row. */
/* Every field on the row is square (asked for) — but not the checkbox,
   which is a checkbox and matches the round ones on the rows above it. */
.todo-quick-add.is-inline select,
.todo-quick-add.is-inline input:not([type="checkbox"]) { border-radius: 0; }
.todo-appointment-date {
  font-size: 0.8rem;
  color: var(--muted);
  flex: none;
  font-variant-numeric: tabular-nums;
}

/* The Meetings page's list/calendar switch. */
.meetings-calendar-nav { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
.meetings-calendar-nav .cal-nav { margin-left: auto; }
.meetings-calendar { margin-bottom: 0.4rem; }
.meetings-calendar .cal-week { min-height: 4.5rem; }

/* Asked for: a hairline between days, so the board reads as a stack of
   days rather than one long run of rows. Not under the last one — a rule
   with nothing below it is just a line. */
.todo-day + .todo-day { border-top: 1px solid var(--border); padding-top: 1rem; }

/* ---------------------------------------------------------------
   Bonnetjes — receipt photos filed under a project.
   --------------------------------------------------------------- */
.receipt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
}
.receipt-card {
  position: relative;
  display: flex;
  gap: 10px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.receipt-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  flex: 0 0 auto;
  max-width: 160px;
}
.receipt-thumb {
  position: relative;
  flex: 0 0 72px;
  width: 72px;
  height: 72px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg-subtle);
}
/* Every photo after the cover is half-size, so a bonnetje with six of them
   doesn't take six times the room in the grid. */
.receipt-thumb:not(.is-cover):not(.receipt-thumb-add),
.receipt-thumb-add { flex-basis: 34px; width: 34px; height: 34px; }
.receipt-thumb-add {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--border);
  color: var(--muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.receipt-thumb-add:hover { border-color: var(--accent); color: var(--accent); }
/* Gevraagd: ook pdf's als bonnetje. Die hebben geen voorvertoning, dus een
   tegeltje met het merkje en (op de eerste) de bestandsnaam. */
.receipt-thumb-pdf { border: 1px solid var(--border); cursor: pointer; }
.receipt-pdf-tegel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 100%;
  height: 100%;
  padding: 2px;
  overflow: hidden;
}
.receipt-pdf-merk {
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--danger, #b3261e);
  color: #fff;
}
.receipt-pdf-naam {
  font-size: 0.58rem;
  line-height: 1.15;
  color: var(--muted);
  text-align: center;
  max-height: 2.3em;
  overflow: hidden;
  word-break: break-all;
}
.receipt-thumb-remove {
  position: absolute;
  top: 1px;
  right: 1px;
  width: 15px;
  height: 15px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
}
.receipt-thumb:hover .receipt-thumb-remove { opacity: 1; }
.receipt-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.receipt-body { min-width: 0; flex: 1; }
.receipt-desc {
  font-weight: 600;
  cursor: text;
  overflow-wrap: anywhere;
}
.receipt-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-top: 4px;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.receipt-amount { font-variant-numeric: tabular-nums; }
.receipt-card .receipt-delete { position: absolute; top: 6px; right: 6px; opacity: 0; }
.receipt-card:hover .receipt-delete { opacity: 1; }

/* A plain centered dialog, used by the timeline's add/edit popover. Kept
   separate from .cal-popover, which anchors itself to the day you clicked;
   there is no equivalent anchor for a timeline bar that may be halfway off
   the side of a scrolling canvas. */
.app-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.35);
}
.app-modal {
  width: min(26rem, 100%);
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  padding: 1rem;
  border-radius: 14px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.app-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.app-modal-head h3 { font-size: 0.95rem; margin: 0; }

/* "Dit is niet opgeslagen" — groter dan een gewoon venster, want dit is het
   enige moment waarop doorwerken je werk kost. De achtergrond is donkerder
   dan normaal en reageert niet op klikken: wegtikken hoort hier niet. */
.save-alert-backdrop { background: rgba(0, 0, 0, 0.55); z-index: 90; }
.save-alert { width: min(34rem, 100%); padding: 1.4rem; }
.save-alert h3 {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
  line-height: 1.3;
  color: var(--danger, #dc2626);
}
.save-alert-uitleg { margin: 0 0 0.8rem; font-size: 0.95rem; line-height: 1.5; }
.save-alert-tips {
  margin: 0 0 0.9rem;
  padding-left: 1.1rem;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--muted);
}
.save-alert-tips li { margin-bottom: 0.3rem; }
.save-alert-melding { margin: 0 0 0.7rem; font-size: 0.85rem; color: var(--muted); }
.save-alert-melding.goed { color: var(--accent); font-weight: 600; }
.save-alert-tekst {
  width: 100%;
  min-height: 7rem;
  margin-bottom: 0.7rem;
  font-size: 0.8rem;
  font-family: inherit;
}
.save-alert-knoppen { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: flex-end; }

.timeline-zoom-slider { flex: 0 1 10rem; min-width: 6rem; accent-color: var(--accent); }

/* An empty to-do: it still occupies a row, says so, and keeps its bin
   visible — with no text there was nothing to hover, and the bin only
   shows on hover, so there was no way to delete one. */
.todo-title.is-empty, .master-todo-title.is-empty { color: var(--muted); font-style: italic; }
.todo-item.is-empty .danger-icon { opacity: 1; }

/* The same "someone else is here" line on a meeting as on a note. It sits
   in the detail pane's action row, so it needs to sit on the baseline with
   the save status rather than above the text the way the note's does. */
.meeting-presence { margin: 0 auto 0 0; }
.meeting-presence.hidden { display: none; }

/* Bonnetjes: the details are real fields on the card, since the flow is
   photo first and everything else afterwards. Quiet until you touch one,
   so a grid of cards still reads as a grid rather than a form. */
.receipt-field {
  border: 1px solid transparent;
  background: transparent;
  border-radius: 6px;
  padding: 2px 4px;
  margin: 0;
  font: inherit;
  color: inherit;
  min-width: 0;
}
.receipt-field:hover { border-color: var(--border); }
.receipt-field:focus { border-color: var(--accent); background: var(--card-bg); outline: none; }
.receipt-field::placeholder { color: var(--muted); font-style: italic; }
.receipt-card .receipt-desc { display: block; width: 100%; font-weight: 600; }
.receipt-card .receipt-amount-input { width: 5.5rem; font-variant-numeric: tabular-nums; }
.receipt-meta .receipt-field { font-size: 0.8rem; color: var(--text-muted); }
.receipt-meta select.receipt-field { max-width: 9rem; }
/* Uploaded but not yet described — the state the quick upload leaves
   behind, and the one to come back to. */
.receipt-card.needs-details { border-left: 3px solid var(--accent); }

/* On a phone the whole toolbar is already a bar pinned bottom-right (see
   .notes-list-toolbar in the phone block above), so "the big button in the
   bottom right corner" is the add button in that bar — filled, so it reads
   as the thing to press rather than a third grey icon. A separate floating
   button would be a second control doing the same job an inch away. */
@media (max-width: 767px) {
  .notes-list-toolbar .receipt-upload-btn {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
  }
  .notes-list-toolbar .receipt-upload-btn .nav-icon svg { width: 20px; height: 20px; }
  /* The bar floats over the grid, so the last card has to end above it. */
  .receipt-grid { padding-bottom: 4.5rem; grid-template-columns: 1fr; }
  /* Photos above the fields rather than beside them: side by side leaves
     the fields a column too narrow to put an amount and a date on one
     line, so every card became five stacked rows. */
  .receipt-card { flex-wrap: wrap; }
  .receipt-thumbs { max-width: none; flex: 1 1 100%; }
  .receipt-body { flex: 1 1 100%; }
  .receipt-meta { align-items: center; }
}
.receipt-upload-btn { cursor: pointer; }

/* Asked for: no left/right padding on the phone notes page. The rows go
   edge to edge the way a phone's own notes list does — the text stays
   inset by the row's own padding, and it is the separators and the
   selected row's highlight that gain by reaching the screen edges.
   Done by pulling the pane out of .view-root's padding rather than
   removing that padding, so anything else on the page (the heading, the
   empty state) stays aligned where it was. */
@media (max-width: 767px) {
  .view-root:has(> .notes-master-detail) { padding-left: 0; padding-right: 0; }
  .view-root:has(> .notes-master-detail) > .section-title { padding-left: 1.5rem; padding-right: 1.5rem; }
  .notes-list-pane { padding-right: 0; }
}

/* Upload progress (asked for). One bar for the whole job, pinned at the
   top of the screen — uploads are started from all over the app (a note,
   a bonnetje, a project cover), and a bar that lives where each of those
   happens to be would need a slot on every one of those layouts. */
#upload-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.4rem 0.9rem 0.5rem;
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
  /* Off-screen rather than display:none, so it slides in and out instead
     of appearing — a bar that blinks on for a fast upload reads as a
     glitch. */
  transform: translateY(-110%);
  transition: transform 160ms ease;
}
#upload-progress.is-visible { transform: translateY(0); }
.upload-progress-bar {
  height: 4px;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}
.upload-progress-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--accent);
  transition: width 120ms linear;
}
.upload-progress-label { font-size: 0.78rem; color: var(--muted); }

/* ---------------------------------------------------------------
   Tabs (asked for) — the browser gesture, inside the app: ctrl/cmd-click,
   middle-click or the right-click menu opens a page in a background tab.
   The strip only exists once there are two of them; with one tab it is
   the app as it was, and a permanent strip holding a single tab is just a
   bar that says where you already are.
   --------------------------------------------------------------- */
/* Below the sidebar breakpoint .app is already a column and the panels are
   its direct children — display:contents keeps them that way, with the tab
   strip simply first in the column. Scoped to the media query rather than
   left as a base rule: this sits after the desktop block in the file, and
   a media query adds no specificity, so unscoped it would win there too
   and collapse the three-panel row. */
@media (max-width: 859.98px) {
  .app-body { display: contents; }
}
.main-column { display: flex; flex-direction: column; }
.tab-strip {
  display: flex;
  align-items: stretch;
  gap: 2px;
  flex: 0 0 auto;
  /* No top padding: the strip starts at the very top of the window, the
     way a browser's own tabs sit directly under the address bar. */
  padding: 0 0.5rem;
  overflow-x: auto;
  scrollbar-width: none;
  /* Asked for: a grey strip with the open tab picked out in white — the
     way a browser separates its chrome from the page. Its own tokens
     rather than --bg/--card-bg, because those two swap places between the
     light and dark themes here and the strip has to stay *behind* the
     active tab in both. Dark gets the same relationship a shade at a
     time: a strip darker than the app, and an active tab matching the
     page below it. */
  background: var(--tab-strip-bg);
  border-bottom: 1px solid var(--border);
}
.tab-strip::-webkit-scrollbar { display: none; }
.tab-strip.hidden { display: none; }
.tab-chip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 1 190px;
  min-width: 78px;
  padding: 0.35rem 0.5rem 0.35rem 0.6rem;
  border: 1px solid transparent;
  /* Asked for: square across the top, rounded at the bottom — the strip
     runs to the very top of the window, so a chip's top edge is the edge
     of the screen and has nothing to round against. */
  border-top: none;
  border-radius: 0 0 9px 9px;
  background: transparent;
  color: var(--muted);
  font-size: 0.82rem;
  cursor: pointer;
  /* The strip scrolls sideways; a chip must not also shrink to nothing. */
  white-space: nowrap;
}
.tab-chip:hover { background: var(--hover-bg, rgba(0, 0, 0, 0.05)); color: var(--text); }
.tab-chip.active {
  background: var(--tab-chip-active-bg);
  border-color: var(--border);
  color: var(--text);
  font-weight: 600;
}
/* Filmplanner draaiend in dit tabblad: hij moet de hoogte krijgen die er is,
   anders kijk je door een kier naar een heel programma. */
.filmplanner-embed-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.filmplanner-frame {
  display: block;
  width: 100%;
  /* De echte hoogte zet sizeFilmplannerFrame() na het tekenen: de ruimte die
     er onder het frame over is. Dit is alleen de stand voor dat moment.
     Géén min-height hier: die was 24rem (384px) en won het van de gemeten
     hoogte, zodat het frame op een laag venster toch te hoog werd en de
     tweede schuifbalk terugkwam. De ondergrens van 360px staat in de JS. */
  height: 70vh;
  /* border-box: de rand telt mee in de gemeten hoogte. Anders stak het frame
     er twee pixels bovenuit, genoeg voor een schuifbalk met niets erin. */
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card-bg);
}

/* Filmplanner draait hier: de ruimte is voor hem. De omslagfoto en de
   sectiekolom links gaan zolang opzij — samen namen ze zoveel breedte dat
   Filmplanner onder zijn eigen 900px-grens zakte en naar zijn telefoon-
   indeling sprong, waar het draaischema niet voor bedoeld is. De balk boven
   het frame houdt de projectnaam en "▤ Samenvatting", en dat brengt alles
   terug. */
.project-detail-root.filmplanner-vol .detail-header,
.project-detail-root.filmplanner-vol .subtabs,
.project-detail-root.filmplanner-vol .add-section-panel { display: none; }
.project-detail-root.filmplanner-vol .project-layout { gap: 0; }

/* flex:1 zodat het label de ruimte vult en het kruisje tegen de rechterrand
   komt te staan. Zonder dat plakte het kruisje aan het eind van de tekst,
   en stond het bij elk tabblad op een andere plek. */
.tab-chip-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The page's own icon, like a favicon. Sized down and dimmed until the tab
   is the active one, so a strip of them reads as one row rather than a
   line of competing colour. */
.tab-chip-icon { display: flex; align-items: center; flex: 0 0 auto; opacity: 0.75; }
.tab-chip.active .tab-chip-icon { opacity: 1; }
.tab-chip-icon .nav-icon svg { width: 14px; height: 14px; }
.tab-chip-icon .project-icon { border-radius: 3px; display: block; }
/* Reordering: the chip follows the pointer's place in the row rather than
   floating free, so the strip always shows where it would land. */
.tab-chip.is-dragging { opacity: 0.65; cursor: grabbing; }
.tab-strip .tab-chip { touch-action: pan-x; }
.tab-chip-close {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
}
.tab-chip:hover .tab-chip-close,
.tab-chip.active .tab-chip-close { opacity: 0.65; }
.tab-chip-close:hover { opacity: 1; background: var(--hover-bg, rgba(0, 0, 0, 0.1)); }
/* Touch has no hover to reveal it, and no middle button either — the
   close button is the only way to shut a tab there. */
@media (hover: none) {
  .tab-chip .tab-chip-close { opacity: 0.65; }
}

/* Navigation progress (asked for): a thin strip of accent colour crossing
   the top of the screen while a page loads. Fixed to the viewport, so it
   is in the same place on a phone and on a desktop regardless of which
   pane is scrolling underneath it. */
#nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 95;
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms ease;
}
#nav-progress.is-loading { opacity: 1; }
/* On the way out it fades while the fill finishes its last stretch, so the
   bar completes rather than vanishing part-way across. */
#nav-progress.is-done { opacity: 0; }
#nav-progress i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  /* A soft leading edge, so it reads as travelling rather than stretching. */
  box-shadow: 0 0 8px var(--accent);
  border-radius: 0 2px 2px 0;
  /* Slow enough to be visible on a fast local load, and eased so most of
     the movement happens early — which is what makes a bar of unknown
     duration feel honest. */
  transition: width 900ms cubic-bezier(0.1, 0.75, 0.2, 1);
}
#nav-progress.is-done i { transition: width 200ms ease-out; }

/* FR#435: on a phone a note's images took most of the screen each, so
   scrolling past a few of them was the whole page. Half the width reads as
   an illustration beside the text rather than a slide. Tapping one still
   opens it full size. */
@media (max-width: 767px) {
  .rich-editable img { max-width: 50%; }
}

/* FR#437: a bonnetje's photo, full size, over the page. */
#lightbox {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lightbox-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.82); }
.lightbox-image {
  position: relative;
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 5rem);
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 12px 50px rgba(0, 0, 0, 0.5);
}
/* Een pdf-bonnetje: de lezer van de browser zelf, in hetzelfde kader. */
.lightbox-pdf {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: min(900px, calc(100vw - 2rem));
  height: calc(100vh - 6rem);
}
.lightbox-pdf iframe {
  flex: 1 1 auto;
  width: 100%;
  border: none;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 12px 50px rgba(0, 0, 0, 0.5);
}
.lightbox-pdf-link {
  align-self: center;
  color: #fff;
  font-size: 0.85rem;
  opacity: 0.8;
}
.lightbox-pdf-link:hover { opacity: 1; }

.lightbox-close,
.lightbox-step {
  position: absolute;
  z-index: 1;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  cursor: pointer;
  line-height: 1;
}
.lightbox-close { top: 1rem; right: 1rem; width: 40px; height: 40px; font-size: 1.5rem; }
.lightbox-step { top: 50%; transform: translateY(-50%); width: 44px; height: 44px; font-size: 2rem; }
.lightbox-step.prev { left: 0.75rem; }
.lightbox-step.next { right: 0.75rem; }
.lightbox-close:hover, .lightbox-step:hover { background: rgba(255, 255, 255, 0.28); }
.lightbox-count {
  position: absolute;
  bottom: 1.1rem;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
/* The thumbnails are buttons now, not links. */
.receipt-thumb { border: none; padding: 0; cursor: zoom-in; }
.receipt-thumb-add { cursor: pointer; }

/* FR#440: dragging a to-do also swept a text selection across whatever the
   pointer passed over — the drag is a pointer gesture, so the browser's own
   text selection was still running underneath it. Suppressed only while a
   drag is actually in progress (the class goes on at the same moment), so
   selecting and copying a to-do's text by hand still works the rest of the
   time. The dragged row itself is included: its own title is the text most
   likely to get caught. */
body.is-dragging {
  user-select: none;
  -webkit-user-select: none;
}

/* Bonnetjes: filtering, selecting, and reading one full size while typing
   its details in beside it. */
.receipts-toolbar .icon-btn.is-on { background: var(--accent); color: #fff; }
.receipt-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.6rem;
  padding: 0.7rem 0.8rem;
  margin-bottom: 0.7rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.receipt-filter-field { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.78rem; color: var(--muted); }
.receipt-filter-field select,
.receipt-filter-field input { margin: 0; }
.receipt-filter-field select[multiple] { min-width: 11rem; }
.receipt-filter-check { display: flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; }
.receipt-filter-check input { width: 16px; height: 16px; margin: 0; }

.receipt-selection-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.5rem 0.8rem;
  margin-bottom: 0.7rem;
  border-radius: 10px;
  background: var(--accent-tint);
  border: 1px solid var(--accent);
  font-size: 0.85rem;
}
.receipt-selection-spacer { flex: 1 1 auto; }

/* The tick and the enlarge control sit on the card, quiet until wanted —
   except once a card is ticked, when its own tick has to stay visible. */
.receipt-pick { position: absolute; top: 6px; left: 6px; z-index: 2; opacity: 0; }
.receipt-expand {
  position: absolute;
  top: 6px;
  right: 30px;
  z-index: 2;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  opacity: 0;
  font-size: 0.9rem;
  line-height: 1;
}
.receipt-card:hover .receipt-pick,
.receipt-card:hover .receipt-expand,
.receipt-card.is-picked .receipt-pick,
.receipt-card.is-expanded .receipt-expand { opacity: 1; }
.receipt-pick input { width: 16px; height: 16px; margin: 0; cursor: pointer; }
.receipt-card.is-picked { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
@media (hover: none) {
  .receipt-pick, .receipt-expand { opacity: 1; }
}

/* Asked for: after uploading, the bonnetje big enough to read with its
   fields beside it. The card spans the whole grid and the photo takes the
   height it needs — copying an amount off a 72px thumbnail is not
   something anyone can do. */
.receipt-card.is-expanded { grid-column: 1 / -1; }
/* A width, not just a cap: .receipt-thumbs sizes to its content by
   default, so with the cover set to width:100% of it the two agreed on
   38px and the lazy photo — being zero-width, and so never "in view" —
   never loaded at all. */
.receipt-card.is-expanded .receipt-thumbs {
  flex: 0 0 auto;
  width: min(52%, 460px);
  max-width: min(52%, 460px);
}
.receipt-card.is-expanded .receipt-thumb.is-cover {
  flex: 1 1 100%;
  width: 100%;
  /* A definite height, not one derived from the photo. Sizing the box to
     the image's intrinsic size is circular when the image is lazy: it has
     no height until it loads, and a zero-height lazy image is never in
     view, so it never loads. The box comes first and the photo fits
     inside it. */
  height: min(62vh, 520px);
  cursor: zoom-in;
}
.receipt-card.is-expanded .receipt-thumb.is-cover img { width: 100%; height: 100%; object-fit: contain; }
/* Een pdf-bonnetje op de opengeklapte kaart: de lezer van de browser, even
   groot als een foto daar zou zijn. */
.receipt-thumb-pdf-groot { position: relative; background: var(--bg-subtle); }
/* Het knopje staat onderin: bovenin zit de werkbalk van de pdf-lezer zelf. */
.receipt-thumb-pdf-groot iframe {
  width: 100%;
  height: 100%;
  border: none;
  border-radius: 8px;
  background: #fff;
}
.receipt-pdf-groter {
  position: absolute;
  bottom: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
  line-height: 1;
}
.receipt-pdf-groter:hover { background: rgba(0, 0, 0, 0.75); }
.receipt-card.is-expanded .receipt-body { align-self: flex-start; }
.receipt-card.is-expanded .receipt-desc { font-size: 1.05rem; }

/* FR#446: on a phone a to-do's text ran off the right of the screen. A
   long unbroken run — a pasted URL, a long compound word — has no break
   opportunity, so the row grew wider than the viewport instead of
   wrapping, and since the page deliberately never scrolls sideways
   (FR#380) the overflow was simply invisible. `anywhere` rather than
   `break-word`: only `anywhere` also lets the row's own min-content width
   shrink, which is what stops the row widening in the first place. The
   editing field gets it too, so the text keeps its shape while edited. */
.todo-item .todo-title,
.todo-item .todo-content .todo-title-input,
.master-todo-title,
.master-todo-item .master-todo-content .todo-title-input {
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* The row and its content column must be allowed to shrink below their
   content's natural width, or the wrapping above never gets a chance. */
.todo-item, .master-todo-item { min-width: 0; }

/* ----------------------------------------------------------------------
   Het geraamte van een pagina die nog laadt

   Gevraagd: van To do's naar Notities duurde soms lang, en zolang dat duurde
   bleef het oude scherm staan — dan lijkt het alsof je druk niet is
   aangekomen. Dit is wat je in die tussentijd ziet: de kop van de pagina
   waar je heen ging, en grijze balken in de vorm van wat er komt. Rustig
   ademend, want een stilstaand grijs blok leest als "kapot".
   ---------------------------------------------------------------------- */
.skelet { padding: 0.2rem 0 1rem; }
.skelet-kop { margin: 0 0 0.9rem; font-size: 1.1rem; }
.skelet-lijst { display: flex; flex-direction: column; gap: 0.55rem; }
.skelet-regel {
  height: 2.6rem;
  border-radius: 10px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  opacity: 0.75;
  animation: skelet-adem 1.4s ease-in-out infinite;
}
.skelet-regel:nth-child(2) { animation-delay: 0.1s; }
.skelet-regel:nth-child(3) { animation-delay: 0.2s; }
.skelet-regel:nth-child(4) { animation-delay: 0.3s; }
.skelet-regel:nth-child(5) { animation-delay: 0.4s; }
.skelet-regel:nth-child(n+6) { animation-delay: 0.5s; }
.skelet-regel.is-breed { height: 4.5rem; }
.skelet-twee { display: grid; grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); gap: 1rem; }
.skelet-vlak { display: flex; flex-direction: column; gap: 0.55rem; }
@media (max-width: 820px) {
  /* Op een telefoon staat de lijst alleen; het venster ernaast bestaat daar
     niet, dus dat hoort ook niet in het geraamte. */
  .skelet-twee { grid-template-columns: minmax(0, 1fr); }
  .skelet-vlak { display: none; }
}
@keyframes skelet-adem {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 0.85; }
}
@media (prefers-reduced-motion: reduce) {
  .skelet-regel { animation: none; opacity: 0.6; }
}
/* Alleen voor een schermlezer: zegt hardop dat de pagina nog laadt, zonder
   dat er iets te zien is (het geraamte zelf zegt dat al met beeld). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ----------------------------------------------------------------------
   Opstartscherm

   Gevraagd: na het herladen bleef het scherm soms lang wit. Dit staat in de
   html zelf, dus het is er al voordat app.js is ingelezen — precies in de
   seconden waarin er anders niets te zien is. Het verdwijnt zodra de app
   iets te tonen heeft.
   ---------------------------------------------------------------------- */
.opstart {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding: 2rem;
  text-align: center;
  background: var(--bg, #f6f6f8);
  color: var(--muted, #6b6b78);
  font-size: 0.9rem;
  /* Wegvallen, niet wegknippen: een harde sprong leest als een storing. */
  transition: opacity 180ms ease;
}
.opstart.is-weg { opacity: 0; pointer-events: none; }
.opstart-ring {
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 50%;
  border: 3px solid var(--border, #d9d9e3);
  border-top-color: var(--accent, #818cf8);
  animation: opstart-draai 0.8s linear infinite;
}
.opstart-tekst { margin: 0; }
.opstart-traag { margin: 0; font-size: 0.82rem; opacity: 0.85; max-width: 22rem; }
@keyframes opstart-draai { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .opstart-ring { animation-duration: 2.4s; }
}

/* Melding als je de pagina sluit terwijl er nog iets wordt opgeslagen
   (zie openstaandeOpslag in app.js). Onderin, midden, goed leesbaar. */
.opslag-melding {
  position: fixed;
  left: 50%;
  bottom: calc(1.2rem + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 3500;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: calc(100vw - 2rem);
  padding: 0.7rem 1rem;
  border-radius: 12px;
  background: var(--card-bg);
  color: var(--text);
  box-shadow: 0 8px 28px rgba(20, 20, 40, 0.22);
  font-size: 0.92rem;
}
.opslag-melding.is-klaar { color: var(--accent); }
.opslag-melding-rondje {
  flex: none;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: opstart-draai 0.8s linear infinite;
}

/* Een to-do die al op het scherm staat maar nog wordt opgeslagen: iets
   lichter, en nog niet aan te klikken (er is nog geen nummer om een
   wijziging naartoe te sturen). Duurt meestal een fractie van een seconde. */
.todo-item.is-pending,
.master-todo-item.is-pending {
  opacity: 0.6;
  pointer-events: none;
}

/* Een to-do die net is ingedeeld (zie add-todo-quick): kort oplichten op
   de plek waar hij terechtkwam, zodat je ziet waar hij naartoe ging. */
.todo-item.is-net-ingedeeld {
  animation: net-ingedeeld 1.6s ease-out;
}
@keyframes net-ingedeeld {
  0%, 35% { background: var(--accent-tint); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .todo-item.is-net-ingedeeld { animation: none; background: var(--accent-tint); }
}

/* ----------------------------------------------------------------------
   Meetings in mappen

   Gevraagd: een mappenstructuur, in te delen op datum, project of persoon.
   Je opent een map, klikt een meeting aan, en gaat met de regel bovenin
   weer terug. De mappen zijn een manier van kijken, geen opslag: bij
   "per persoon" staat dezelfde meeting in de map van iedereen die erbij was.
   ---------------------------------------------------------------------- */
.meeting-folder-list { padding: 0.2rem 0; }
.meeting-folder-row,
.meeting-folder-back {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 0.7rem;
  border-radius: 8px;
  cursor: pointer;
}
.meeting-folder-row:hover,
.meeting-folder-back:hover { background: var(--bg); }
.meeting-folder-icon { display: inline-flex; color: var(--muted); flex: none; }
/* Een map zonder icoon (de projecten) mag ook de ruimte ervan niet houden. */
.meeting-folder-icon:empty { display: none; }
.meeting-folder-icon .nav-icon { width: 1.05rem; height: 1.05rem; }
.meeting-folder-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.92rem;
}
.meeting-folder-meta { flex: none; font-size: 0.78rem; color: var(--muted); }
/* De terugregel hoort bij de lijst eronder, niet ertussen: iets steviger,
   met een lijn die hem van de meetings scheidt. */
.meeting-folder-back {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
  border-radius: 8px 8px 0 0;
  font-weight: 600;
}
.meeting-folder-back-pijl { flex: none; color: var(--muted); }
/* Het pad waar je staat, klein onder de terugregel: Project › 2026 › September */
.meeting-kruimels {
  flex: none;
  max-width: 55%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.72rem;
  color: var(--muted);
  font-weight: 400;
}

/* ----------------------------------------------------------------------
   Meetings: één blad in plaats van een lijst naast de tekst

   Gevraagd: je begint in de mappen, en zodra je een meeting opent neemt die
   het hele werkblad in — alles tussen het menu links en de rand rechts. De
   knoppen en de terugregel staan bovenin; onderin staat niets meer, zodat
   daar geen ruimte meer verloren gaat.
   ---------------------------------------------------------------------- */
/* Gevraagd: terug naar de opmaak van notities — links navigeren, rechts het
   gesprek. De maten komen daar vandaan (.notes-master-detail); hier staat
   alleen wat Meetings extra heeft. */

/* Gevraagd: het pijltje terug naast de kop "Meetings", zonder de naam van
   de vorige pagina erachter. */
.meetings-paginakop {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
/* Gevraagd: op een groot scherm hoeft het pijltje terug niet. De lijst
   staat daar altijd naast het gesprek, dus er valt niets te "verlaten" —
   je klikt gewoon het volgende kopje of gesprek aan. Op een telefoon wél:
   daar schuift het gesprek over de lijst heen en is dit de weg terug. */
.meetings-terug { display: none; }
@media (max-width: 859.98px) {
  .meetings-terug {
    display: inline-flex;
    flex: 0 0 auto;
    font-size: 1.1rem;
    line-height: 1;
    padding: 0.1rem 0.35rem;
  }
}
/* Waar je bent, boven de lijst. Binnen een project staat het pijltje
   ervoor, want daar is geen paginakop om het naast te zetten. */
.meetings-kruimelrij {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0 0.1rem 0.5rem;
  min-width: 0;
}
.meetings-kruimelrij .meeting-kruimels {
  font-size: 0.8rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Gevraagd: maanden (en personen) als uitklapbare kopjes, met de maand waar
   je in zit al open — in plaats van mappen waar je doorheen moet klikken. */
.meeting-sectie { border-bottom: 1px solid var(--border); }
.meeting-sectie-kop {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  width: 100%;
  padding: 0.55rem 0.5rem;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.meeting-sectie-kop:hover { background: var(--bg); }
.meeting-sectie-pijl {
  flex: 0 0 auto;
  width: 0.9rem;
  color: var(--muted);
  font-size: 0.8rem;
}
.meeting-sectie-naam {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.meeting-sectie-meta {
  flex: 0 0 auto;
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--muted);
}
/* Gevraagd: de kopjes binnen meetings blijven zwart. Een open kopje in de
   accentkleur trok de aandacht naar iets wat je toch al ziet — het staat
   open. */
/* Een kopje binnen een project (de maanden): iets kleiner en ingesprongen,
   zodat je ziet wat waarbij hoort. */
.meeting-sectie.is-genest { border-bottom: none; }
.meeting-sectie.is-genest > .meeting-sectie-kop {
  padding-left: 1.4rem;
  font-weight: 500;
  font-size: 0.92rem;
}
.meeting-sectie.is-genest .meeting-list-row { padding-left: 2.4rem; }
.meeting-sectie-lijst .meeting-list-row { padding-left: 1.6rem; }

/* In- en uitklappen met een beweging (gevraagd), op dezelfde manier als de
   projecten op het to-do-bord: van de eigen hoogte naar nul, zonder die
   hoogte te hoeven meten. */
.meeting-sectie-lijst {
  overflow: hidden;
  transition: height 180ms ease, opacity 180ms ease;
}
.meeting-sectie.is-dicht > .meeting-sectie-lijst {
  height: 0;
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .meeting-sectie-lijst { transition: none; }
}

/* Gevraagd: het gesprek in de rechterkolom krijgt dezelfde marge als een
   notitie daar — het plakte tegen de scheidingslijn en de rechterrand aan. */
.meetings-master-detail > .notes-detail-pane { padding: 0 0.7rem 0.7rem 0.9rem; }

/* De prullenbak bij het gesprek zelf (gevraagd: het was onduidelijk hoe je
   er een weggooit), naast het vinkje "Gehad" op de veldenregel. */
.meeting-detail-actions { display: flex; align-items: center; gap: 0.4rem; }
.meeting-verwijder-vak { display: flex; flex-direction: column; }
.meeting-verwijder-vak > label { visibility: hidden; }
.meeting-verwijder {
  align-self: flex-start;
  color: var(--danger, #b3261e);
}
.meeting-verwijder:hover { border-color: var(--danger, #b3261e); }

/* De drie knoppen bovenin de lijstkolom (gevraagd): Recent, Per project,
   Op persoon. */
.meeting-weergave { padding-top: 0.1rem; }

/* De keuze tussen mappen per maand en mappen per persoon, boven de mappen
   binnen een project. Zichtbaar, want in het ☰-menu vond niemand hem. */
.meeting-indeling {
  display: flex;
  flex-wrap: wrap;            /* in een smal blad komen ze onder elkaar */
  gap: 0.3rem;
  padding: 0.5rem 0.2rem 0.6rem;
}
.meeting-indeling-knop {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  min-width: 0;
  white-space: nowrap;        /* liever naast elkaar dan een woord gebroken */
  padding: 0.28rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--muted);
  font-size: 0.82rem;
  cursor: pointer;
}
.meeting-indeling-knop .nav-icon { width: 0.95rem; height: 0.95rem; }
.meeting-indeling-knop:hover { color: var(--text); }
.meeting-indeling-knop.is-actief {
  background: var(--accent-tint);
  border-color: var(--accent);
  color: var(--accent);
}

/* Grijze balkjes voor een stuk van Home dat nog onderweg is (zie
   vulHomeSecties). Rustig ademend, zodat stilstand niet als storing leest. */
.home-wacht { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.2rem 0; }
.home-wacht-blok {
  height: 3.4rem;
  border-radius: 10px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  opacity: 0.6;
  animation: skelet-adem 1.4s ease-in-out infinite;
}
.home-wacht-blok:nth-child(2) { animation-delay: 0.15s; }

/* ----------------------------------------------------------------------
   Urenregistratie: regels gegroepeerd

   Gevraagd: binnen een project urenregels aan elkaar kunnen groeperen, met
   de uren per groep bij elkaar opgeteld. De kop draagt de naam en het
   totaal; de regels eronder horen erbij.
   ---------------------------------------------------------------------- */
.uren-groep-kop {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  margin: 1rem 0 0.35rem;
  padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--border);
}
.uren-groep-kop:first-child { margin-top: 0.2rem; }
.uren-groep-naam { font-weight: 600; font-size: 0.95rem; }
.uren-groep-totaal { font-size: 0.85rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.uren-groep-kop.is-los .uren-groep-naam { font-weight: 500; color: var(--muted); }
/* De knop draagt de naam van de groep waarin deze regel zit; zonder groep
   is het een uitnodiging, dus rustiger. */
.uren-groep-knop { max-width: 10rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uren-groep-knop.is-gezet { color: var(--accent); border-color: var(--accent); }

/* Subproject kiezen in een urenformulier, en het vinkje "gefactureerd". */
.uren-subkeuze { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.uren-subkeuze-select { min-width: 11rem; }
.uren-subkeuze-naam { min-width: 11rem; }
.uren-sub-tarief { margin-left: 0.5rem; }
.uren-sub-leeg { padding: 0.5rem 0.2rem; font-size: 0.85rem; }

.uren-tarief-veld { width: 8.5rem; }
.uren-eigen-tarief { color: var(--accent); }
.uren-subnaam { color: var(--accent); }
.uren-gefactureerd { color: var(--ok, #0d9488); }
.uren-factuur-vink {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.85rem;
  color: var(--muted);
  cursor: pointer;
}
.uren-factuur-vink:has(input:checked) {
  color: var(--ok, #0d9488);
  border-color: var(--ok, #0d9488);
  background: var(--card-bg);
}
/* Wat er in een blok nog openstaat: de reden dat je hier kijkt. */
.uren-nog-open { color: var(--accent); }

/* Gevraagd: de uren en het geld per subproject bij elkaar opgeteld, los van
   hoe de lijst eronder is ingedeeld. */
.uren-subtotalen {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.5rem 0.7rem;
  margin-bottom: 1rem;
}
.uren-subtotalen-kop {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.uren-subtotaal-rij {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.22rem 0;
  font-size: 0.9rem;
}
.uren-subtotaal-naam { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uren-subtotaal-uren { flex: none; font-variant-numeric: tabular-nums; color: var(--muted); }
.uren-subtotaal-geld { flex: none; font-variant-numeric: tabular-nums; font-weight: 600; min-width: 5.5rem; text-align: right; }
.uren-subtotaal-open { flex: none; font-size: 0.8rem; color: var(--accent); min-width: 7rem; text-align: right; }

/* "Gedeclareerd?" bij een bonnetje (gevraagd). */
.receipt-declared {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.78rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}
.receipt-declared.is-aan { color: var(--ok, #0d9488); border-color: var(--ok, #0d9488); }

/* Knopje boven het to-do-bord van een project: de gekoppelde agenda erbij
   tonen of juist niet (gevraagd). */
.todo-board-balk { display: flex; justify-content: flex-end; margin-bottom: 0.4rem; }

/* Gevraagd: op een groot scherm hoeven de titels "Projecten", "To do's" en
   "Meetings" niet boven die pagina's — links in het menu staat al opgelicht
   waar je bent, dus de kop zegt het een tweede keer en kost een regel. Op
   een telefoon blijft hij: daar is dat menu er niet. */
@media (min-width: 860px) {
  .kop-verbergen-op-desktop { display: none; }
  /* De regel eromheen mag dan ook geen hoogte meer innemen als er verder
     niets in staat. */
  .section-title:has(> .kop-verbergen-op-desktop:only-child) { display: none; }
}

/* Gevraagd: lucht boven "Recente notities" en de losse notities, zodat dat
   blok niet tegen de wensenlijst erboven aan plakt. */
.home-notities-kop { margin-top: 1.6rem; }
.home-losse-notities:not(:empty) { margin-top: 1.2rem; }

/* Gevraagd: "Terug naar projecten" weg op een groot scherm — daar staat het
   projectmenu links al open, dus die knop is een tweede weg naar hetzelfde.
   Op een telefoon was hij al verborgen (zie de regel bij
   .project-detail-root verderop, waar de balk onderin de weg terug is), dus
   hiermee is hij overal uit beeld. De tekst staat er in het Nederlands voor
   het geval hij ooit weer tevoorschijn komt. */
.project-terug { display: none; }
/* Het icoon in die knop op de maat van de tekst houden (gemeld: het was er
   veel te groot). */
.todo-board-balk button { display: inline-flex; align-items: center; gap: 0.35rem; }
.todo-board-balk button .nav-icon svg { width: 1em; height: 1em; }
.todo-board-balk .is-actief { color: var(--accent); border-color: var(--accent); }

/* Gevraagd: op een telefoon geen keuzelijst voor het project in het veld
   voor een nieuwe to-do — die neemt daar te veel van de regel in beslag.
   Je kunt het project daar nog steeds kiezen door "CODE: " voor de tekst te
   typen, of de to-do achteraf naar een project slepen. */
@media (max-width: 820px) {
  .todo-quick-add select[name="project_id"] { display: none; }
}
