/* ---------------------------------------------------------------
   Suitable Suite — shared theme
   Design tokens for light + dark, layered on Bootstrap 5.3's
   native `data-bs-theme` support.
   --------------------------------------------------------------- */

:root {
  --brand: #5b5ef4;
  --brand-hover: #4547d0;
  --brand-contrast: #fff;
  --brand-ring: rgba(91, 94, 244, 0.15);
  --brand-glow: rgba(91, 94, 244, 0.12);
  --brand-soft-bg: #ededfd;
  --brand-soft-bg-strong: #c7c8f8;
  /* The suite's own mark — the wordmark in the bar and on the auth pages —
     is the one thing that keeps the indigo when an app recolours --brand:
     it names the suite, not the app, and the icon on the home screen is this
     colour whichever app was open. */
  --suite-brand: #5b5ef4;

  /* The raised surfaces are pure white, so the page has to carry the whole
     contrast step. At #f4f6fb it was 3% off white and cards vanished into
     it; this is deep enough to read as a backdrop without going grey. */
  --app-bg: #e8ecf5;
  --surface: #fff;
  --surface-hover: #eef0f4;

  --border-strong: #9ca3af;
  --border-soft: #d1d5db;
  --border-faint: #e5e7eb;

  --text-strong: #111827;
  --text-body: #374151;
  --text-muted: #6b7280;
  --text-faint: #9ca3af;
  --text-danger: #dc2626;

  /* The soft fills were 100-level tints: amber-100 came out at 1.11:1 against
     a white card, so the badge shape vanished and only the text read. Low and
     Medium sit two steps deeper; High and Urgent are solid, so the two levels
     worth noticing carry the most weight. Every label clears 4.5:1 on its own
     fill, which is what caps how far each one can go. Urgent takes the same
     red as --text-danger, so it matches the overdue date sitting beside it. */
  --prio-1-bg: #d1d5db; --prio-1-fg: #1f2937;
  --prio-2-bg: #fbbf24; --prio-2-fg: #78350f;
  --prio-3-bg: #7c3aed; --prio-3-fg: #fff;
  --prio-4-bg: #dc2626; --prio-4-fg: #fff;

  /* The optional card tints, one per due band: a fill and the edge that goes
     round it. The fill alone was the first attempt and it did not survive a
     bright screen — a tint pale enough to leave the text on it alone is a few
     per cent off white, which is nothing at arm's length outdoors. The edge is
     where the saturation goes instead, since nothing is written on it.

     Red is deeper than the other two rather than the same step below white: a
     phone warming its own display — True Tone, Night Shift — flattens a pale
     pink into plain paper while leaving amber and blue looking like tints. */
  --due-overdue-bg: #fac9c9; --due-overdue-edge: #d33f3f;
  --due-soon-bg: #fceec2; --due-soon-edge: #d9a020;
  --due-later-bg: #dae6fb; --due-later-edge: #6d97dd;

  --auth-shadow: 0 8px 32px rgba(91, 94, 244, 0.08);
  --elevation: 0 1px 2px rgba(17, 24, 39, 0.06), 0 4px 12px rgba(17, 24, 39, 0.07);
}

[data-bs-theme="dark"] {
  /* Lift the brand hue so it stays legible on dark surfaces. */
  --brand: #8385f8;
  --brand-hover: #9b9dfa;
  --brand-contrast: #14161b;
  --brand-ring: rgba(131, 133, 248, 0.25);
  --brand-glow: rgba(131, 133, 248, 0.18);
  --brand-soft-bg: #262a3d;
  --brand-soft-bg-strong: #343a56;
  --suite-brand: #8385f8;

  --app-bg: #14161b;
  --surface: #1c1f27;
  --surface-hover: #262a33;

  --border-strong: #454b59;
  --border-soft: #343a45;
  --border-faint: #2a2f38;

  --text-strong: #f3f4f6;
  --text-body: #d1d5db;
  --text-muted: #9aa3b2;
  --text-faint: #6b7280;
  --text-danger: #f87171;

  --prio-1-bg: #343a45; --prio-1-fg: #d1d5db;
  --prio-2-bg: #4a3a12; --prio-2-fg: #fcd34d;
  --prio-3-bg: #6d28d9; --prio-3-fg: #fff;
  --prio-4-bg: #6b2727; --prio-4-fg: #fca5a5;  /* was #4c1d1d, only 1.18:1 on the card */

  /* Deep rather than pale: a tint lighter than --surface would make the
     coloured cards the brightest thing on a dark page, which is the opposite
     of what a quiet backdrop with a few urgent cards on it should look like.
     The edges are lifted off the fills instead, where the light theme's are
     dropped below them — the same step, taken in the direction that reads. */
  --due-overdue-bg: #3b2226; --due-overdue-edge: #8a4046;
  --due-soon-bg: #3a3117; --due-soon-edge: #8a7020;
  --due-later-bg: #1e2c42; --due-later-edge: #3f6396;

  --auth-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  --elevation: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.25);

  /* Align Bootstrap's own surfaces with the app palette. */
  --bs-body-bg: var(--app-bg);
  --bs-body-color: var(--text-body);
  --bs-emphasis-color: var(--text-strong);
  --bs-secondary-color: var(--text-muted);
  --bs-border-color: var(--border-soft);
}

/* Bootstrap declares color-scheme for its dark theme but never resets it for
   light, so a reader on a dark OS who has chosen light keeps a dark canvas
   behind the page — the overscroll band, the form controls the UA draws
   itself, and on Android whatever shows through the transparent navigation
   bar. The pair has to be complete for the toggle to work in both directions. */
[data-bs-theme="light"] { color-scheme: light; }

/* Bootstrap components that should sit on the raised surface, not the page. */
.card { --bs-card-bg: var(--surface); }
.modal-content { --bs-modal-bg: var(--surface); }
.dropdown-menu {
  --bs-dropdown-bg: var(--surface);
  /* The chosen filter reads as this app's selection rather than Bootstrap's
     own blue, which appears nowhere else on the page. */
  --bs-dropdown-link-active-bg: var(--brand);
  --bs-dropdown-link-active-color: var(--brand-contrast);
}

/* Every card in the templates is `card border-0 shadow-sm`. Bootstrap's
   `.border-0` is `border:0!important`, and its `shadow-sm` is a 4px blur at
   7.5% black — so a white card on a light page had no edge at all. Restore
   the same outline `.todo-card` uses, which needs `!important` and the extra
   `.border-0` specificity to beat the utility class. */
.card.border-0 { border: 1px solid var(--border-strong) !important; }
.card.shadow-sm { box-shadow: var(--elevation) !important; }

/* One hover for every card the reader picks from a list — a todo, a project,
   a tag, a note, a journal entry — so the cue means the same thing in every
   app. The ring, not just the edge: swapping a 1px border for another 1px
   border is a clear cue on a white card and almost none on a card already
   edged in its due band, where the line was saturated to begin with. A
   spread shadow thickens it without moving anything, so the row does not
   shift under the pointer. The `!important` is for the two overrides above,
   which the Bootstrap cards carry and the todo card does not; an entry card
   holding a form is the one being edited, not one to pick. */
.todo-card,
.entry-card,
.note-card,
.tag-row {
  transition: box-shadow 0.15s, border-color 0.15s;
}
.todo-card:hover,
.entry-card:not(:has(form)):hover,
.note-card:hover,
.tag-row:hover {
  box-shadow: 0 0 0 2px var(--brand), 0 4px 16px var(--brand-glow) !important;
  border-color: var(--brand) !important;
}

body {
  background: var(--app-bg);
  min-height: 100vh;
}

/* --- App chrome ------------------------------------------------ */

.app-navbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border-faint);
}

.navbar-brand {
  font-weight: 700;
  letter-spacing: -0.5px;
  color: var(--suite-brand) !important;
}

/* The app tabs beside the brand, above `md`. The same fills as the sidebar's
   links, so the current app reads as selected the way the current page does
   below it — and in that app's own accent, which is the switch made visible
   in the bar itself. */
.app-tab {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  min-height: 44px;
  border-radius: 8px;
  color: var(--text-body);
  text-decoration: none;
  font-weight: 500;
}
.app-tab:hover {
  background: var(--brand-soft-bg);
  color: var(--brand);
}
.app-tab.active {
  background: var(--brand-soft-bg-strong);
  color: var(--brand);
  font-weight: 600;
}
/* The tabs line up with the page, not the brand: they sit over the sidebar's
   edge, in line with the main column's content, so the bar reads as two
   columns the way the page under it does. The brand is given the sidebar's
   width (less the bar's own 24px padding) to push them there, and the tabs
   take the main column's 24px padding for their own. `md` because that is
   where the sidebar appears — the same breakpoint the tabs do. */
@media (min-width: 768px) {
  .navbar-brand {
    min-width: calc(var(--sidebar-width) - 24px);
    margin-right: 0;
  }
  .app-tabs { margin-left: 24px; }
}

/* The app switcher: the menu that took the place of the Sign out button.
   Each item is a thumb-sized line, icon then label, so the row of apps reads
   like the sidebar does — and the sign-out form inside it is a plain item
   like the rest. */
.app-switcher-menu { --bs-dropdown-min-width: 11rem; }
.app-switcher-menu .dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
}
.app-switcher-menu .dropdown-item i { width: 20px; text-align: center; }
.app-switcher-menu form { margin: 0; }

/* Only ever on screen above `md` — below that the same links are in the
   navbar's collapse — so its width is a plain rule rather than a query. The
   width is a token because the navbar's brand is sized to it, so the app
   tabs start where the main column does. */
:root { --sidebar-width: 220px; }
.sidebar {
  width: var(--sidebar-width);
  padding: 24px 16px;
  min-height: calc(100vh - 56px);
  border-right: 1px solid var(--border-faint);
  background: var(--surface);
}

/* A column of cards stops being readable long before it stops being wide.
   The cap does nothing on a phone, which is the point: it is the desktop
   enhancement, not the layout. */
.app-main { max-width: 860px; }

/* The unread count on the menu button. Below `md` the nav is folded away, so
   the badge on the Notifications link is behind the very button that would
   have to be pressed to see it — this one sits on the outside of it. */
.navbar-toggler { position: relative; }
.toggler-badge {
  position: absolute;
  top: 0;
  right: 0;
  font-size: 0.6rem;
  padding: 0.15em 0.4em;
  transform: translate(35%, -25%);
}
/* Shared by the desktop sidebar and the navbar's collapsible mobile menu. */
.app-nav .nav-link {
  color: var(--text-body);
  border-radius: 8px;
  padding: 8px 12px;
  margin-bottom: 2px;
}
.app-nav .nav-link:hover {
  background: var(--brand-soft-bg);
  color: var(--brand);
}
/* The current page needs to read as selected at a glance, not just as
   "hovered a moment ago", so it gets a deeper fill than hover, plus weight
   so the cue does not rest on background contrast alone. */
.app-nav .nav-link.active {
  background: var(--brand-soft-bg-strong);
  color: var(--brand);
  font-weight: 600;
}
.app-nav .nav-link i { width: 20px; }
/* The count sits at the far end of the link rather than beside the label, so
   it lands in the same place on every item and does not shove "Notifications"
   around as it grows from one digit to two. */
.app-nav .nav-link {
  display: flex;
  align-items: center;
  gap: 2px;
}
.app-nav .nav-link .badge {
  margin-left: auto;
  font-size: 0.7rem;
  font-weight: 600;
}
/* Roomier hit areas in the mobile menu, where there is no pointer to aim with. */
.navbar .app-nav .nav-link { padding: 11px 12px; }

/* --- Theme toggle ---------------------------------------------- */

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: transparent;
  color: var(--text-body);
  cursor: pointer;
  transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}
.theme-toggle:hover {
  background: var(--surface-hover);
  color: var(--brand);
  border-color: var(--brand);
}
.theme-toggle .bi { font-size: 1.1rem; line-height: 1; }
/* Show the icon for the theme a click would switch *to*. */
.theme-toggle .icon-dark { display: none; }
[data-bs-theme="dark"] .theme-toggle .icon-dark { display: inline-block; }
[data-bs-theme="dark"] .theme-toggle .icon-light { display: none; }

.theme-toggle-floating {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 10;
}

/* --- Install button -------------------------------------------- */

/* The auth pages have no navbar to hang it off, so it sits beside the
   floating toggle — far enough left to clear the toggle's 44px box, which is
   what a thumb needs it to be. */
.install-app-floating {
  position: fixed;
  top: 1rem;
  right: 4.25rem;
  z-index: 10;
}
@media (min-width: 768px) and (pointer: fine) {
  /* Back to clearing a 32px box. */
  .install-app-floating { right: 3.5rem; }
}

/* --- Filter bar ------------------------------------------------ */

/* One bar, two elements: a GET form holding the search field and the menus
   that submit with it, and the page's own Filter button, which is a menu of
   links and submits nothing. `display: contents` drops the form's box so its
   children join this flex row directly — as a flex item of its own the form
   could only ever be a block the button wrapped below, which is what left the
   button stranded on a line of its own. The `.w-100` inside the form is the
   line break: the field takes the first line, the menus and the button the
   next. */
.filter-bar > form { display: contents; }

/* Safari zooms the page in when it focuses a text field whose text is under
   16px, and does not zoom back out — so the one small field in the app is
   full-size text on a phone whatever the row around it is. */
.input-group-sm > .form-control { font-size: 1rem; }
@media (min-width: 768px) and (pointer: fine) {
  .input-group-sm > .form-control { font-size: 0.875rem; }
}

/* On a phone the two menus split whatever the Filter button leaves of the
   line, so the bar reads as one block of controls rather than as three
   different widths falling where they land. With room to lay out they go
   back to being as wide as their longest option. */
.filter-bar .filter-menu {
  flex: 1 1 0;
  min-width: 0;
}
@media (min-width: 768px) {
  .filter-bar .filter-menu { flex: 0 0 auto; }
}

/* --- Priority counts ------------------------------------------ */

/* A line of small text is four links, and each is a thumb target: so
   `.priority-btn`'s trick again, a box round each without the line growing
   to a bar of four 44px buttons. The reach sideways is about half the
   separator, so neighbouring boxes meet under the dot and no further. The
   underline says link for the reason `.project-link` gives; the one already
   in force is bold, because that is what the closed Filter menu is hiding. */
.priority-count {
  position: relative;
  white-space: nowrap;
}
a.priority-count {
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
a.priority-count:hover,
a.priority-count:focus-visible { color: var(--brand-hover); }
a.priority-count.active { font-weight: 600; }
a.priority-count::before {
  content: "";
  position: absolute;
  inset: -12px -5px;
}

/* --- Layout helpers -------------------------------------------- */

/* Lets a flex child shrink below its content width so `.text-truncate`
   and normal wrapping can take effect. Bootstrap ships no equivalent. */
.min-w-0 { min-width: 0; }

/* Two lines, then an ellipsis. `.text-truncate` is the desktop version of
   this idea and it fails on a phone: one line of a narrow column is three
   words, so everything past "Marketing si..." is lost rather than shortened. */
.line-clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* --- Todo cards ------------------------------------------------ */

.todo-card {
  /* Padding and gap belong to the card rather than to `p-3 gap-3` in the
     markup: the phone size is not on Bootstrap's scale, and a utility that
     had to be beaten back with `!important` at every width was the old way
     of saying the same thing. */
  padding: 0.75rem;
  gap: 0.5rem;
  /* The fill and the edge come from the due band when there is one and the
     reader has asked for it — set as variables there rather than as
     properties here, or a `.colour-coded .todo-card.due-*` rule would outrank
     :hover and swallow the one cue a card already edged in its band has. */
  background: var(--card-fill, var(--surface));
  border: 1px solid var(--card-edge, var(--border-strong));
  border-radius: 12px;
  /* Long prose in notes and meta wraps at word boundaries.
     `.text-truncate` sets white-space:nowrap, which opts its own
     elements out of this. */
  overflow-wrap: break-word;
  /* A card is an anchor: the priority picker posts back to the page and comes
     back to `#todo-<pk>`. Landing flush against the top edge reads as the top
     of the list rather than as the place the reader was, so leave the card the
     gap it had between it and the one above. */
  scroll-margin-top: 0.75rem;
}
/* Pinned cards are out of the sort order everything else obeys, so the section
   has to say why it is sitting at the top — and a frame says where it stops as
   well, which an edge on each card never did. The cards inside keep their own
   borders and carry no pin marker of their own: two brand lines a card apart
   only ask which of them is the section. The padding is what the hover ring on
   the outermost cards lands in. */
.pinned-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem;
  border: 1px solid var(--brand);
  /* One step outside the cards' 12px, so the frame reads as sitting around
     them rather than tracing them. */
  border-radius: 14px;
}
/* The gap under the box, kept off it when the section runs to the foot of what
   has been loaded — there is nothing there yet to be separated from. */
.pinned-group:has(+ .todo-card) { margin-bottom: 1rem; }
/* A run longer than one chunk arrives as a box per chunk, because a chunk can
   only box what it was given. The seam is closed rather than drawn: the two
   inner edges go, and the negative margin eats the list's gap down to the gap
   the cards inside already sit at — so "Show more" grows the section instead
   of opening a second box under it. */
.pinned-group:has(+ .pinned-group) {
  border-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.pinned-group + .pinned-group {
  margin-top: -1rem;
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.todo-card.completed { opacity: 0.55; }
/* The project card's "Complete" badge sits inside .project-name but is an
   inline-block, so the strikethrough stops at it rather than crossing it out. */
.todo-card.completed .todo-title,
.todo-card.completed .project-name {
  text-decoration: line-through;
}
.todo-card.completed .todo-title { color: var(--text-faint); }
/* A finished project keeps its place on Projects for good, where a completed
   todo is on its way off the list — so it has to stay readable rather than
   merely recognisable, and the fade over faint text together took it under
   2:1 on a white card. The badge and the strikethrough already say "done";
   the de-emphasis only has to sit the row below the live ones. `.project-name`
   is what tells the two cards apart, since they share the rest of the markup. */
.todo-card.completed:has(.project-name) { opacity: 0.8; }
.todo-card.completed .project-name { color: var(--text-muted); }
/* De-emphasis marks completed todos out as the background against active
   ones. On the Completed page every card is done, so there is nothing to
   contrast against and the whole list just reads as washed out. The
   strikethrough stays on as the "done" marker. */
.all-completed .todo-card.completed { opacity: 1; }
.all-completed .todo-card.completed .todo-title { color: inherit; }

/* Off unless the reader has asked for it on the Preferences page, which is
   what puts `.colour-coded` on the body — so the band a card carries costs
   nothing until then. Deliberately the card's fill rather than another badge:
   the point is to find the pressing work without reading anything, and the
   date and the Overdue badge already say it in words for anyone who is. */
.colour-coded .todo-card.due-overdue { --card-fill: var(--due-overdue-bg); --card-edge: var(--due-overdue-edge); }
.colour-coded .todo-card.due-soon { --card-fill: var(--due-soon-bg); --card-edge: var(--due-soon-edge); }
.colour-coded .todo-card.due-later { --card-fill: var(--due-later-bg); --card-edge: var(--due-later-edge); }

/* The date goes back to reading as text on a card that is already red: red on
   red is the one pairing the fill cannot carry, and it is the fill saying the
   thing now. The Overdue badge beside it is untouched, so the word is still
   there for anyone the colour is not reaching. */
.colour-coded .todo-card.due-overdue .overdue { color: var(--text-body); }

/* Titles can be a single unbroken token (a URL, a slug). `break-word` alone
   keeps min-content at the full token width, so the title would render on
   top of the row actions; `anywhere` shrinks min-content so it wraps inside
   its own column instead. Scoped to titles to keep short meta labels like
   "Q3 Planning" from breaking mid-word. */
.todo-title,
.project-name { overflow-wrap: anywhere; }

/* A card whose body opens a page — a project, the todo a notification came
   from. The hover ring was the only thing saying so, and a phone has no
   hover: the row read as text and the reader had to press it to find out.
   The chevron is the cue that survives a touchscreen. It is muted because it
   labels the row rather than being a control of its own — the whole row is
   the target, which is why the link keeps the card's own colour instead of
   turning the name into a line of blue. */
.row-link {
  color: inherit;
  text-decoration: none;
}
.row-link-chevron {
  color: var(--text-faint);
  flex-shrink: 0;
}
/* The pointer already has the ring; this just aims it at what the row does. */
.todo-card:hover .row-link-chevron { color: var(--brand); }

/* The project on a todo card: the one part of the card that opens another
   page, sitting in a row of plain meta text. `.row-link`'s problem again and
   the same reasoning — a phone cannot hover, so the cue has to be there at
   rest — but the opposite answer, because here the link is a few words inside
   a line rather than the whole row: a chevron would be pointing at nothing in
   particular, so it is the underline that says link and the colour that says
   which words. Left muted it read as more grey text with an icon in front.

   The height is the thumb's, not the text's: 0.875rem of underlined text is
   about 20px, and the row it sits in is the only thing on the card with room
   to give. Shrunk back for a mouse with everything else below. */
.project-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.project-link:hover,
.project-link:focus-visible { color: var(--brand-hover); }

/* On a phone the card cannot afford wider gutters: the toggle, two 1rem gaps
   and 1rem of padding leave the title barely a word wide, and
   `overflow-wrap: anywhere` then breaks it mid-word. The room arrives at sm,
   so the roomier version is the enhancement. */
@media (min-width: 576px) {
  .todo-card {
    padding: 1rem;
    gap: 1rem;
  }
}

/* Sized here rather than by an inline style on each of the three templates
   that render one. A badge is a label beside the title, so it is set against
   the tag chips it shares the line with rather than against Bootstrap's
   `.badge` default, which is an em of whatever it happens to sit in. */
.priority-badge { font-size: 0.8rem; }

.priority-badge-1 { background: var(--prio-1-bg); color: var(--prio-1-fg); }
.priority-badge-2 { background: var(--prio-2-bg); color: var(--prio-2-fg); }
.priority-badge-3 { background: var(--prio-3-bg); color: var(--prio-3-fg); }
.priority-badge-4 { background: var(--prio-4-bg); color: var(--prio-4-fg); }

/* --- Priority picker ------------------------------------------- */

/* The badge is the control that sets what it reports, so it is a button on
   every todo still to be done. Borderless and in the page's own font, because
   a `<button>` brings a chrome border and the platform's font with it and the
   badge beside it on the Completed page brings neither. The caret is
   Bootstrap's own `.dropdown-toggle`, so the thing that says "this opens a
   menu" is the same mark here as on the Filter button further up the page —
   which is why the tap area below moved to ::before and left ::after to it. */
.priority-btn {
  position: relative;  /* for the enlarged tap area in ::before */
  border: 0;
  font-family: inherit;
  cursor: pointer;
}

/* `.toggle-btn`'s trick again: a badge grown to 44px would read as a heading
   next to the title, so the thumb gets a box round it instead of a bigger
   badge. Sideways as well as up and down — "Low" is barely 36px wide — and
   the room either side is the card's own gaps, where nothing else is
   clickable. */
.priority-btn::before {
  content: "";
  position: absolute;
  inset: -12px -6px;
  border-radius: 12px;
}

.priority-menu { --bs-dropdown-min-width: 9rem; }

/* The item names the priority and shows it: the four fills are what the
   reader actually recognises in the list, and the words alone would make
   choosing one a matter of remembering which colour it turns into. */
.priority-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
}
.priority-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.tag-chip {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 20px;
  font-size: 0.8rem;
  font-weight: 500;
  /* Each chip overrides this with whichever of black or white reads better on
     its own fill (the readable_on filter); white is what is left for a colour
     too malformed to judge. */
  color: #fff;
  line-height: 1.6;
}

.overdue { color: var(--text-danger); }

.toggle-btn {
  position: relative;  /* for the enlarged tap area in ::after */
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 2px solid var(--text-muted);
  background: var(--surface);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.toggle-btn.done {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}

.empty-state {
  text-align: center;
  padding: 48px 0;
  color: var(--text-faint);
}
@media (min-width: 768px) {
  .empty-state { padding: 80px 0; }
}
.empty-state i { font-size: 3rem; display: block; margin-bottom: 1rem; }

/* --- Row actions ----------------------------------------------- */

/* The things that can be done to a row — view, pin, edit, delete — are one
   button on a phone and a row of buttons on a desktop, out of a single copy
   of the markup. Three or four 44px targets beside a title is over a third
   of a 390px screen, and the title pays for it in mid-word breaks; behind one
   menu button the card is a title again. Above `md`, where there is both
   room and a pointer to aim with, the menu stops being a menu and lays its
   own items out in a line. */
.row-actions-menu { --bs-dropdown-min-width: 11rem; padding: 0.25rem; }
.row-actions-menu.show {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.row-actions-menu form { margin: 0; }
.row-action {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}

@media (min-width: 768px) {
  .row-actions-toggle { display: none; }
  /* `!important` beats the inline styles Popper leaves behind if the menu was
     opened at a narrower width and the window then grew. */
  .row-actions-menu,
  .row-actions-menu.show {
    display: flex !important;
    flex-direction: row;
    position: static !important;
    transform: none !important;
    inset: auto !important;
    /* The 11rem a menu needs to fit its labels is dead space once the labels
       are gone: it is in the flow here, so the width is the card's, and the
       icons sat at the left of it with the title paying for the rest. */
    min-width: 0;
    gap: 0.25rem;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    box-shadow: none;
  }
  .row-action { width: auto; }
  /* The icon is the whole button here; the name lives in its `title`. */
  .row-action-label { display: none; }
}

/* --- Buttons & form controls ----------------------------------- */

.btn-brand {
  background: var(--brand);
  color: var(--brand-contrast);
  border: none;
}
.btn-brand:hover {
  background: var(--brand-hover);
  color: var(--brand-contrast);
}

/* A date field is as wide as the browser's own `dd/mm/yyyy` and the picker
   icon it draws beside it, and when it runs out of room it clips instead of
   wrapping: at 320px the twelfths leave the field 132px where it wants 140,
   and the last `y` goes under the icon. So each of the pair says what it
   needs and the row wraps on the phones too narrow for both — one field per
   line, each filling it, which is how the rest of the form reads at that
   width anyway. Anything with room for the two is untouched. */
.due-date-col, .due-time-col { flex-grow: 1; }
.due-date-col { min-width: 156px; } /* 140 for the field, 16 for the gutter */
.due-time-col { min-width: 101px; } /* 85 for `--:--` and its clock, likewise */

/* Above `sm` the grid can have the row back, though not as equal thirds: at
   `md` the sidebar takes enough of it that a third is 139px, a pixel short of
   what the date wants, so the date is given five twelfths and the clock — the
   narrower field by far — three. The phone minimum goes off with it, or it
   wraps that same narrow `md` row. */
@media (min-width: 576px) {
  .due-date-col,
  .due-time-col {
    flex-grow: 0;
    min-width: 0;
  }
}

.form-control, .form-select { border-color: var(--border-strong); }
.form-control:focus, .form-select:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-ring);
}

.btn-outline-secondary {
  border-color: var(--border-strong);
  color: var(--text-body);
}
.btn-outline-secondary:hover {
  background: var(--surface-hover);
  border-color: var(--text-muted);
  color: var(--text-strong);
}

.btn-outline-danger { border-color: var(--border-strong); }
.btn-outline-danger:hover { border-color: var(--text-danger); }

/* Bootstrap pins an alert's dismiss button to the top right corner, which
   only looks centred while the message is one line long. On a phone most of
   them are two, and the X then floats above the text it closes. */
.alert-dismissible .btn-close {
  top: 50%;
  transform: translateY(-50%);
}

/* --- Touch targets --------------------------------------------- */

/* A thumb is the default aim. The icon-only controls are the mis-tap risk —
   small squares, with a destructive Delete among them — so every one of them
   holds a ~44px target, and the compact sizing further down is what a screen
   has to earn. */

/* Keep the 26px box, but extend the tappable area around it. */
.toggle-btn::after {
  content: "";
  position: absolute;
  inset: -9px;
  border-radius: 12px;
}

.todo-card .btn-sm,
.empty-state .btn-sm,
.detail-actions .btn-sm,
.entry-card .btn-sm,
.tag-row .btn-sm {
  min-width: 44px;
  min-height: 44px;
}

.navbar-toggler { min-width: 44px; min-height: 44px; }

/* Filter menus and page-header buttons. */
.btn-sm { min-height: 38px; min-width: 44px; }

/* A box given a height taller than its contents — `min-height` here, an
   `.input-group` stretching its children elsewhere — only grows downwards. A
   `<button>` centres its own contents whatever height it is handed, but a
   link styled as one is an ordinary box, so the room a thumb needs collects
   under the label and it rides high in the target. Centring is what makes the
   grown box read as a button again. Row actions are the exception: inside the
   phone menu they are icon-then-label lines that all have to start at the
   same left edge, so they keep the flex they set for themselves. */
a.btn:not(.row-action) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Wide *and* pointer-driven: a touchscreen is a thumb however big it is, so
   the width alone is not enough to shrink anything. */
@media (min-width: 768px) and (pointer: fine) {
  .toggle-btn::after { content: none; }
  .priority-btn::before { content: none; }
  .priority-option { min-height: 0; }
  .project-link { min-height: 0; }

  .todo-card .btn-sm,
  .empty-state .btn-sm,
  .detail-actions .btn-sm,
  .entry-card .btn-sm,
  .tag-row .btn-sm,
  .navbar-toggler,
  .btn-sm,
  .calendar-nav .btn {
    min-width: 0;
    min-height: 0;
  }
  .calendar-day { min-height: 36px; }
  .app-switcher-menu .dropdown-item { min-height: 0; }
  .app-tab { min-height: 0; }

  .theme-toggle { width: 32px; height: 32px; }
  .theme-toggle .bi { font-size: 0.95rem; }
}

/* --- Preferences ----------------------------------------------- */

/* Bootstrap's switch is a 32x16 box, which is a thumb's width short. Clicking
   the label toggles it too, so the row is what gets the height rather than the
   switch being blown up out of all proportion to the words beside it. Left at
   this size on a desktop as well: nothing is competing for the row, so there
   is nothing to win back by shrinking it. */
.pref-switch { min-height: 44px; padding-left: 3.5em; }
.pref-switch .form-check-input {
  width: 2.5em;
  height: 1.25em;
  /* Bootstrap's own pair is 2.5em/-2.5em, which leaves the label hard against
     a switch this wide; both go out together or the two stop lining up. */
  margin-left: -3.5em;
  margin-top: 0.15em;
}
/* On is the app's own accent, as every other chosen thing on the page is —
   Bootstrap's blue appears nowhere else. */
.pref-switch .form-check-input:checked {
  background-color: var(--brand);
  border-color: var(--brand);
}

/* The settings Save carries are ringed together with the button itself, so a
   reader can see where its reach ends: the notifications switch below the ring
   is the browser's own subscription and acts the moment it is flipped, and a
   page of controls with one button under the lot of them says the opposite. */
.pref-saved {
  max-width: 600px;
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  /* Two more edges to pay for across 390px, and every card inside the ring
     already carries its own padding, so the phone gets the thinner gutter. */
  padding: 0.5rem;
}
@media (min-width: 576px) {
  .pref-saved { padding: 1rem; }
}

/* --- Journal --------------------------------------------------- */

/* Its accent, over the todo app's indigo: the two were built to read as
   different things when both were open, and inside one shell the colour is
   what says which of them is. The window furniture is unaffected — the bar
   and splash colours read --surface and --app-bg, which do not change. */
body[data-app="journal"] {
  --brand: #0f766e;
  --brand-hover: #115e59;
  --brand-ring: rgba(15, 118, 110, 0.18);
  --brand-glow: rgba(15, 118, 110, 0.12);
  --brand-soft-bg: #e0f2f0;
  --brand-soft-bg-strong: #b5e2dc;
  --auth-shadow: 0 8px 32px rgba(15, 118, 110, 0.10);
}
[data-bs-theme="dark"] body[data-app="journal"] {
  --brand: #2dd4bf;
  --brand-hover: #5eead4;
  --brand-ring: rgba(45, 212, 191, 0.25);
  --brand-glow: rgba(45, 212, 191, 0.18);
  --brand-soft-bg: #1f3331;
  --brand-soft-bg-strong: #274641;
}

/* The day page is two columns from `lg` up, and the todo app's cap leaves
   the calendar a sliver beside the entries. */
body[data-app="journal"] .app-main { max-width: 1100px; }

/* Stacked above the entries (below `lg`) the column is the full page, and a
   month grid stretched across a 900px tablet is a row of 44px-tall slivers.
   Cap it at about a phone's width there; beside the entries it fills the
   column it was given. */
.journal-side { max-width: 460px; }
@media (min-width: 992px) {
  .journal-side { max-width: none; }
}

/* A month is seven equal columns whatever the width. `table-layout: fixed`
   is what stops "Wed" or a two-digit day from widening its column and
   pushing the grid past a 390px screen. */
.calendar { table-layout: fixed; width: 100%; margin-bottom: 0; }
.calendar th {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
  padding: 0.25rem 0;
  border: 0;
}
.calendar td { padding: 1px; border: 0; }

/* The whole cell is the link, and the cell is the tap target: a day number
   on its own is a 10px thing no thumb can be asked to hit. */
.calendar-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  border-radius: 8px;
  color: var(--text-body);
  text-decoration: none;
  font-size: 0.9rem;
  line-height: 1;
  position: relative;
}
.calendar-day:hover { background: var(--brand-soft-bg); color: var(--brand); }
.calendar-day.outside { color: var(--text-faint); }
.calendar-day.today { font-weight: 700; color: var(--brand); }
.calendar-day.selected,
.calendar-day.selected:hover {
  background: var(--brand);
  color: var(--brand-contrast);
}
/* The dot under a day that has entries. On the selected day it is drawn in
   the contrast colour, or it vanishes into the fill. */
.calendar-day .dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--brand);
  margin-top: 4px;
}
.calendar-day.selected .dot { background: var(--brand-contrast); }
/* Reserve the dot's room on every day so the numbers line up across the row
   whether or not a day has something under it. */
.calendar-day .dot.empty { background: transparent; }

.calendar-nav .btn { min-width: 44px; min-height: 44px; }

.entry-card .card-body { white-space: normal; }
/* Shared with the notes app: a note is prose in a card too, and the rules
   below are about prose, not about the journal.
   The body is Markdown, rendered by suite.markdown with a newline kept as a
   line break, so line breaks and paragraphs are the renderer's business now,
   not white-space's. A pasted URL is the thing with no break in it, and the
   title is as likely to hold one as the body. What is left to CSS is fitting
   a document's furniture into a card: headings a step or two down from the
   page's own, no margin trailing off the bottom, and the two things that can
   be wider than a phone — a table and a code block — scrolling inside
   themselves rather than taking the page sideways with them. */
.entry-body { overflow-wrap: anywhere; }
.entry-body > :first-child { margin-top: 0; }
.entry-body > :last-child { margin-bottom: 0; }
.entry-body h1, .entry-body h2, .entry-body h3, .entry-body h4, .entry-body h5, .entry-body h6 {
  font-weight: 700; margin: 0.75rem 0 0.25rem;
}
.entry-body h1 { font-size: 1.25rem; }
.entry-body h2 { font-size: 1.125rem; }
.entry-body h3, .entry-body h4, .entry-body h5, .entry-body h6 { font-size: 1rem; }
.entry-body p, .entry-body ul, .entry-body ol, .entry-body blockquote, .entry-body pre, .entry-body table {
  margin-bottom: 0.75rem;
}
.entry-body ul, .entry-body ol { padding-left: 1.5rem; }
.entry-body blockquote { border-left: 3px solid var(--border-strong); padding-left: 0.75rem; color: var(--text-muted); }
.entry-body pre { overflow-x: auto; padding: 0.5rem 0.75rem; background: var(--surface-hover); border-radius: 6px; }
.entry-body table { display: block; overflow-x: auto; max-width: 100%; }
.entry-body th, .entry-body td { border: 1px solid var(--border-soft); padding: 0.25rem 0.5rem; }
.entry-body img { max-width: 100%; height: auto; }
.entry-body hr { margin: 0.75rem 0; }
/* A task list's boxes are a record, not controls: the renderer disables
   them, and the greyed look a disabled box gets says as much. */
.entry-body ul.contains-task-list { list-style: none; padding-left: 0.25rem; }
.entry-body .task-list-item-checkbox { margin-right: 0.35rem; vertical-align: -0.1em; }
/* The preview stands where the textarea stood, so it is drawn as one: the
   same border, and tall enough not to make the form jump on a short body. */
.markdown-preview {
  min-height: 8rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--bs-border-radius);
  background: var(--surface);
}
.entry-title { overflow-wrap: anywhere; }
.entry-meta { font-size: 0.8rem; color: var(--text-muted); }

/* The journal's chips are links (to the entries carrying the tag); the todo
   app's are plain spans, so the shared .tag-chip rule says nothing about it. */
a.tag-chip { text-decoration: none; }
a.tag-chip:hover { filter: brightness(1.1); color: inherit; }

/* The list of tags in the sidebar: chip-shaped links, with the one being
   filtered on outlined so it reads as pressed. */
.tag-list .tag-chip { margin: 0 4px 6px 0; }
.tag-list .tag-chip.active { outline: 2px solid var(--text-strong); outline-offset: 1px; }

/* On the Tags page the whole row is the link, so the row is what reacts
   (with the hover ring every list card shares), not the chip inside it. */
.tag-row .tag-chip:hover { filter: none; }

/* The day heading over a group of search results is a link back to that
   day, in the brand colour rather than Bootstrap's link blue, which appears
   nowhere else on the page. */
.result-day { color: var(--brand); }
.result-day:hover { color: var(--brand-hover); }

/* --- Notes ----------------------------------------------------- */

/* Its accent: sky blue, over the todo's indigo and the journal's teal. The
   same tokens the journal overrides, for the same reason. It was amber
   once, and amber buttons read as a warning beside the other two apps. */
body[data-app="notes"] {
  --brand: #0369a1;
  --brand-hover: #075985;
  --brand-ring: rgba(3, 105, 161, 0.18);
  --brand-glow: rgba(3, 105, 161, 0.12);
  --brand-soft-bg: #e0f2fe;
  --brand-soft-bg-strong: #bae6fd;
  --auth-shadow: 0 8px 32px rgba(3, 105, 161, 0.10);
}
[data-bs-theme="dark"] body[data-app="notes"] {
  --brand: #38bdf8;
  --brand-hover: #7dd3fc;
  --brand-ring: rgba(56, 189, 248, 0.25);
  --brand-glow: rgba(56, 189, 248, 0.18);
  --brand-soft-bg: #17303f;
  --brand-soft-bg-strong: #1e4258;
}

/* Every notes page is one column of prose. A line of text much past 70
   characters is hard to follow back to its start, and a list of cards
   stretched across a wide screen is a row of thin slivers with a date
   floating far from its title. */
.notes-column { max-width: 720px; }

/* A note in a list is a link the size of the whole card, so the card is what
   reacts, with the hover ring every list card shares — and being a link, its
   text has to be told to stay put rather than turn Bootstrap's link blue. The
   title is the first line of whatever was written, which is as likely to be
   a URL as a heading, so it may not be allowed to overflow. */
.note-card, .note-card:hover { color: var(--text-body); }
.note-card .note-title { display: block; overflow-wrap: anywhere; }
.note-card .note-excerpt { overflow-wrap: anywhere; }

/* The editor grows with the writing rather than scrolling inside a box of
   twelve rows: on a phone an inner scroll bar inside the page's own is two
   things fighting over one thumb. The element in the selector is what beats
   Bootstrap's own `textarea.form-control` minimum. */
textarea.note-editor { min-height: 12rem; field-sizing: content; }

/* The way from the front page to the full list, and from a search back to
   it: in the accent rather than Bootstrap's link blue, which appears nowhere
   else on the page. */
.notes-column a.more-link { color: var(--brand); }
.notes-column a.more-link:hover { color: var(--brand-hover); }

/* --- Auth pages (login, register, password reset) --------------- */

.auth-body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
.auth-card {
  background: var(--surface);
  border-radius: 16px;
  border: 1px solid var(--border-strong);
  padding: 2.5rem 2rem;
  width: 100%;
  max-width: 420px;
  box-shadow: var(--auth-shadow);
}
.brand {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--suite-brand);
  letter-spacing: -0.5px;
}
.field-error { color: var(--text-danger); font-size: 0.8rem; margin-top: 0.25rem; }
.form-text { color: var(--text-muted); font-size: 0.8rem; }

.auth-card .nav-tabs {
  border-bottom: 2px solid var(--border-faint);
  margin-bottom: 1.5rem;
}
.auth-card .nav-tabs .nav-link {
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  color: var(--text-muted);
  padding: 0.5rem 1rem;
  font-weight: 500;
}
.auth-card .nav-tabs .nav-link.active {
  color: var(--brand);
  border-bottom-color: var(--brand);
  background: none;
}
