/* ProjectManager interface styles.
   Single stylesheet, no build step and no CDN request: the two typefaces are
   served from static/fonts, so the interface looks the same offline.
   Layout is fluid: tables scroll horizontally inside their wrapper and the
   navigation collapses behind a toggle on narrow screens.

   The palette and typography follow the Urban Water Management and
   Hydroinformatics Group site (uwmh.eu): sand #b09980 as the brand accent,
   navy #20295a, cyan #0e9cd1, warm grey #ecece7, with Dosis for headings and
   navigation and Source Sans for body text. Two deliberate departures, both
   for legibility at application densities: text is set in navy rather than
   sand, which fails contrast against white; and the cyan used for interactive
   text is darkened to #0a6f95 to clear WCAG AA. The undarkened cyan is kept
   for fills and rules where nothing has to be read off it. */

@font-face {
  font-family: "Dosis";
  src: url("../fonts/dosis-latin.87bc734caa77.woff2") format("woff2");
  font-weight: 200 800;
  font-display: swap;
}

@font-face {
  font-family: "Source Sans 3";
  src: url("../fonts/source-sans-3-latin.30164609c163.woff2") format("woff2");
  font-weight: 200 900;
  font-display: swap;
}

:root {
  --sand: #b09980;
  --sand-dark: #96805f;
  --sand-pale: #f3efe9;
  --navy: #20295a;
  --navy-dark: #161d40;
  --cyan: #0e9cd1;
  --cyan-text: #0a6f95;
  --mist: #ecece7;

  --bg: #f5f5f5;
  --surface: #ffffff;
  --border: #ddd9d2;
  --text: #21232b;
  --muted: #6a6459;
  --accent: var(--navy);
  --accent-dark: var(--navy-dark);
  --success: #1f7a4d;
  --warning: #9a6608;
  --danger: #a52a2a;
  --info: var(--cyan-text);
  --heading-font: "Dosis", "Trebuchet MS", system-ui, sans-serif;
  --body-font: "Source Sans 3", "Source Sans Pro", system-ui, -apple-system,
    "Segoe UI", Roboto, Arial, sans-serif;
  --radius: 6px;
  --gap: 1rem;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--body-font);
  font-size: 16px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
}

.container { width: min(1400px, 100% - 2rem); margin-inline: auto; }
/* A page can drop the 1400px cap and use the whole window (PM-21). */
.layout-wide .container { width: calc(100% - 2rem); }

a { color: var(--accent); }
a:hover { color: var(--cyan-text); }

h1, h2, h3, h4, .brand, .site-nav a, .stat__value, .button {
  font-family: var(--heading-font);
}
h1, h2, h3, h4 { color: var(--navy); letter-spacing: .01em; }
h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 .25rem; }
h2 { font-size: 1.3rem; font-weight: 600; margin: 0 0 .75rem; }
h3 { font-size: 1.1rem; font-weight: 600; margin: 1rem 0 .5rem; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--accent); color: #fff; padding: .5rem 1rem;
}
.skip-link:focus { left: 0; }

:focus-visible { outline: 3px solid var(--cyan); outline-offset: 2px; }

/* Keeps the sand footer band at the bottom of a short page. */
body { display: flex; flex-direction: column; min-height: 100vh; }

/* ----------------------------------------------------- impersonation bar

   Loud on purpose. The failure mode of "view as another user" is forgetting
   you are in it and acting under someone else's name, so the banner stays on
   every page and the whole viewport is outlined while it is active. */
.impersonation-bar {
  background: var(--danger); color: #fff; font-size: .9rem;
  padding: .5rem 0; position: sticky; top: 0; z-index: 50;
}
.impersonation-bar__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap;
}
.impersonation-bar .button { padding: .2rem .7rem; font-size: .85rem; }
body.is-impersonating { box-shadow: inset 0 0 0 3px var(--danger); }

/* ------------------------------------------------------------- header */

.site-header {
  background: var(--surface);
  border-bottom: 3px solid var(--sand);
  box-shadow: 0 1px 3px rgba(33, 35, 43, .07);
}
.site-header__inner {
  display: flex; align-items: center; gap: var(--gap);
  flex-wrap: wrap; padding: .55rem 0;
}
.brand {
  display: flex; align-items: center; gap: .7rem;
  text-decoration: none; color: var(--navy); flex: none;
}
.brand__logo { display: block; width: 172px; height: auto; }
.brand__name {
  font-size: 1.05rem; font-weight: 700; line-height: 1.1;
  text-transform: uppercase; letter-spacing: .06em;
  padding-left: .7rem; border-left: 2px solid var(--sand);
}
.site-nav { display: flex; gap: .85rem; flex: 1 1 auto; flex-wrap: wrap; justify-content: flex-end; }
.site-nav a {
  text-decoration: none; padding: .3rem 0; color: var(--navy);
  font-weight: 600; font-size: .98rem; border-bottom: 2px solid transparent;
}
.site-nav a:hover { color: var(--sand-dark); border-bottom-color: var(--sand); }
.site-header__user {
  display: flex; align-items: center; gap: .5rem;
  flex: none; padding-left: .9rem; border-left: 1px solid var(--border);
}
.nav-toggle {
  display: none; background: none; border: 1px solid var(--border);
  border-radius: var(--radius); font-size: 1.1rem; padding: .25rem .6rem;
  cursor: pointer; color: var(--navy);
}
.nav-toggle:hover { border-color: var(--sand); color: var(--sand-dark); }

/* ------------------------------------------------------ user menu

   A <details> disclosure: the browser handles opening, closing, keyboard
   operation and the state it announces, so the menu works without JavaScript.
   The default triangle marker is removed in favour of the caret in the markup,
   which can then be rotated to show the open state. */
.user-menu { position: relative; }
.user-menu__trigger {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .3rem .6rem; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface);
  color: var(--navy); font-family: var(--heading-font); font-weight: 600;
  cursor: pointer; list-style: none; white-space: nowrap;
}
.user-menu__trigger::-webkit-details-marker { display: none; }
.user-menu__trigger::marker { content: ""; }
.user-menu__trigger:hover { border-color: var(--sand); background: var(--sand-pale); }
.user-menu[open] .user-menu__trigger { border-color: var(--sand); background: var(--sand-pale); }
.user-menu__caret { font-size: .7rem; transition: transform .12s ease; }
.user-menu[open] .user-menu__caret { transform: rotate(180deg); }

.user-menu__panel {
  position: absolute; right: 0; top: calc(100% + .35rem); z-index: 60;
  min-width: 13rem; padding: .3rem;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 6px 18px rgba(33, 35, 43, .16);
}
.user-menu__meta {
  margin: 0; padding: .35rem .6rem .5rem;
  border-bottom: 1px solid var(--border);
  font-size: .8rem; color: var(--muted); white-space: nowrap;
}
.user-menu__panel a,
.user-menu__signout {
  display: block; width: 100%; text-align: left;
  padding: .45rem .6rem; border: 0; border-radius: 3px;
  background: none; color: var(--navy);
  font: inherit; font-size: .92rem; text-decoration: none; cursor: pointer;
}
.user-menu__panel a:hover,
.user-menu__signout:hover { background: var(--sand-pale); color: var(--navy); }
.user-menu__signout { border-top: 1px solid var(--border); margin-top: .3rem; }

/* The panel would otherwise hang off the right edge of a narrow screen. */
@media (max-width: 1100px) {
  .user-menu__panel { right: auto; left: 0; }
}

@media (max-width: 1100px) {
  .nav-toggle { display: block; order: 2; margin-left: auto; }
  .site-nav {
    display: none; flex-basis: 100%; order: 4;
    flex-direction: column; gap: .2rem; justify-content: flex-start;
  }
  .site-nav.is-open { display: flex; }
  .site-header__user { order: 3; padding-left: 0; border-left: 0; }
}

.breadcrumbs { padding-top: .75rem; font-size: .875rem; }
.breadcrumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; }
.breadcrumbs li + li::before { content: "/"; margin-right: .4rem; color: var(--muted); }

main { padding: 1.25rem 0 3rem; }

/* The sand band mirrors the footer of uwmh.eu. */
.site-footer {
  background: var(--sand); color: #2b2419;
  font-size: .85rem; padding: 1.15rem 0; margin-top: auto;
}
.site-footer a { color: #2b2419; }
.site-footer p { margin: .15rem 0; }
.site-footer__brand { font-family: var(--heading-font); font-weight: 700; letter-spacing: .04em; }

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

.messages { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .5rem; }
.message { padding: .65rem .9rem; border-radius: var(--radius); border: 1px solid; }
.message--success, .message--info { background: #e4f3fa; border-color: #a5d6ea; }
.message--warning { background: #fdf5e5; border-color: #e6cf9a; }
.message--error, .message--danger { background: #fbeded; border-color: #e3b3b3; }

/* -------------------------------------------------------------- layout */

.page-header {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: 1rem;
}
.actions { display: flex; gap: .5rem; flex-wrap: wrap; }

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1rem; margin-bottom: 1rem;
}
/* A panel heading and its options on one row, dropping to two when narrow. */
.panel__header {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: .75rem;
}
.panel__header h2 { margin-bottom: .1rem; }
/* The project page's UWMH/all switch. It is a link, not a form control, so it
   keeps the .button look but loses the paragraph margin that would push it out
   of line with the heading beside it. */
.panel__scope { margin: 0; }
.panel__scope .button { padding: .25rem .6rem; font-size: .8rem; }
.panel--accent { border-left: 4px solid var(--sand); }
.panel--danger { border-left: 4px solid var(--danger); }
/* The change history closes each detail page. It is reference material rather
   than part of the record, so it gets a lighter box than the panels above it. */
.panel--quiet { background: var(--sand-pale); padding: .6rem 1rem; }
.panel-grid { display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
/* A grid item's default min-width is auto, so a wide table would push its track
   past 1fr and leave the columns uneven. Pinning it to zero lets the table
   scroll inside its panel instead (PM-35). */
.panel-grid > * { min-width: 0; }

.muted, .help { color: var(--muted); font-size: .875rem; }
/* Descriptions run the full width of their panel. The usual ~75ch measure is
   easier on the eye, but these sit alone in a wide panel under a full-width
   definition list, and a narrow column of text beside all that empty space
   reads as a layout fault rather than as typography. */
.prose { white-space: normal; max-width: 100%; }
/* Notes are working commentary, not part of the record's own description --
   the sand rule keeps the two apart without a second panel. */
.prose--notes {
  border-left: 3px solid var(--sand); padding: .1rem 0 .1rem .75rem;
  background: var(--sand-pale); border-radius: 0 var(--radius) var(--radius) 0;
}

/* A <details> disclosure for long text that need not be read every visit.
   Same approach as the user menu: the browser handles opening, keyboard
   operation and the state it announces, so no JavaScript is involved. The
   trigger is styled to read as the <h3> it replaces. */
.disclosure { margin: 1rem 0 .5rem; }
.disclosure__trigger {
  display: inline-flex; align-items: center; gap: .35rem;
  font-family: var(--heading-font); font-size: 1.1rem; font-weight: 600;
  color: var(--navy); letter-spacing: .01em;
  cursor: pointer; list-style: none;
}
.disclosure__trigger::-webkit-details-marker { display: none; }
.disclosure__trigger::marker { content: ""; }
.disclosure__trigger:hover { color: var(--sand-dark); }
.disclosure__caret { font-size: .7rem; transition: transform .12s ease; }
.disclosure[open] .disclosure__caret { transform: rotate(180deg); }
.disclosure > .prose { margin: .5rem 0 0; }

/* The same disclosure a weight down, for the change history. The summary
   carries the newest change's timestamp so the reader can judge whether
   opening it is worth the click. */
.disclosure--history { margin: 0; }
.disclosure--history .disclosure__trigger {
  flex-wrap: wrap; font-size: .95rem; color: var(--muted);
}
.disclosure--history .disclosure__trigger:hover { color: var(--navy); }
.disclosure__meta {
  font-family: var(--body-font); font-size: .8rem; font-weight: 400;
  color: var(--muted);
}
.disclosure--history .table { margin-top: .75rem; margin-bottom: 0; }
.disclosure--history .empty-state { margin: .75rem 0 0; }

.empty-state {
  color: var(--muted); background: var(--sand-pale); border: 1px dashed var(--sand);
  border-radius: var(--radius); padding: 1rem; text-align: center;
}

/* --------------------------------------------------------------- stats */

.stats { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); margin-bottom: 1rem; }
.stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .85rem; text-decoration: none; color: inherit; display: block;
}
.stat--alert { border-color: var(--danger); }
.stat__value { display: block; font-size: 1.6rem; font-weight: 700; }
.stat__label { display: block; color: var(--muted); font-size: .85rem; }

/* The panel is a standing reminder rather than a place to read, so it stays as
   short as two lines: heading with its explanation, then the breakdown. */
.unassigned-summary { padding: .75rem 1rem; }
.unassigned-summary__heading { margin-bottom: .35rem; }
/* The explanation rides along inside the heading, so it has to shed the
   heading's font and weight or it reads as a second title. */
.unassigned-summary__note {
  font-family: var(--body-font); font-weight: 400; font-size: .875rem;
  color: var(--muted);
}

/* The five figures behind the unassigned tile: a wrapping row rather than a
   list, so they read as one breakdown of a single total. */
.unassigned-summary__counts {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .35rem 1.25rem; color: var(--muted);
}
.unassigned-summary__counts strong { color: var(--text); font-size: 1.1rem; }
/* The way out of the panel, not a sixth figure: it keeps to the end of the row
   and drops back to the running text size. */
.unassigned-summary__review { margin-left: auto; font-size: .875rem; }
.unassigned-heading { display: flex; align-items: center; gap: .5rem; }

/* -------------------------------------------------------------- tables */

/* The scroll belongs to the table, never to the page: the cap stops a wide
   table from widening the panel around it. */
.table-wrap { overflow-x: auto; max-width: 100%; }
.table { width: 100%; border-collapse: collapse; background: var(--surface); }
.table th, .table td { padding: .55rem .6rem; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; }
.table thead th {
  background: var(--mist); font-family: var(--heading-font); font-weight: 700;
  font-size: .9rem; white-space: nowrap; color: var(--navy);
}
.table thead th a { text-decoration: none; color: inherit; }
.table--compact th, .table--compact td { padding: .4rem .5rem; font-size: .9rem; }
.row--overdue td:first-child { border-left: 3px solid var(--danger); }
.row--error td { background: #fbeded; }
/* A row ticked for removal is still submitted -- it just will not survive the
   save -- so it is faded rather than hidden, and its Remove box stays legible
   so the tick can be taken back. */
.row--removed td { opacity: .45; text-decoration: line-through; }
.row--removed td:last-child { opacity: 1; text-decoration: none; }
.cell--links { max-width: 20ch; }
.cell--months { white-space: nowrap; color: var(--muted); font-size: .85rem; }
.cell--code { white-space: nowrap; }

/* "Due soon" mixes deliverables and milestones in one date-ordered list, so a
   reader scanning it has to be able to tell them apart without reading the Type
   column. The deliverable -- the weightier record -- carries an accent edge and
   a bolder code; the milestone stays quiet. */
.row--deliverable > td:first-child { border-left: 3px solid var(--accent); }
.row--deliverable .cell--code a { font-weight: 700; }
.row--milestone > td:first-child { border-left: 3px solid transparent; }
.row--milestone .cell--code a { font-weight: 400; }
.row--milestone > td { color: var(--muted); }
.row--milestone .cell--code a,
.row--milestone td a { color: inherit; }
/* A ToDo is one person's own work rather than something the project owes, so
   it gets its own dashed edge: present enough to be told from a milestone's
   blank one, quiet enough not to compete with the deliverable's solid accent. */
.row--todo > td:first-child { border-left: 3px dashed var(--cyan-text); }

/* Late beats kind. The same list also carries rows that are already overdue,
   and being late is the more urgent of the two things a row can say, so the
   danger edge wins over the accent one -- and a late milestone is not the quiet
   record the muted colour makes it. Placed after the kind rules, which have the
   same specificity and would otherwise win on source order. */
.row--overdue > td:first-child { border-left: 3px solid var(--danger); }
.row--overdue.row--milestone > td { color: inherit; }

/* Project Results (PM-55) carry the same distinction everywhere deliverables
   and milestones are listed together: the deliverable's title is set bold with
   its code, and a list item outside a table takes the same weights. The type
   badge beside each always names the kind, so weight is never the only cue. */
.row--deliverable .cell--title { font-weight: 700; }
.result--deliverable > a { font-weight: 700; }
.result--milestone > a { font-weight: 400; color: var(--muted); }
.type-badge { vertical-align: middle; }

/* -------------------------------------------------------------- badges */

.badge {
  display: inline-block; padding: .1rem .45rem; border-radius: 999px;
  font-size: .75rem; font-weight: 600; border: 1px solid; white-space: nowrap;
}
.badge--neutral { background: var(--mist); border-color: #d3d3cc; color: #45483f; }
.badge--info { background: #e4f3fa; border-color: #a5d6ea; color: var(--cyan-text); }
.badge--success { background: #e9f5ee; border-color: #a9d8bf; color: var(--success); }
.badge--warning { background: #fdf3e2; border-color: #e8cd93; color: var(--warning); }
.badge--danger { background: #fbeaea; border-color: #e5b0b0; color: var(--danger); }
.badge--muted { background: #f1f0ed; border-color: #dad7d1; color: var(--muted); }
/* The two kinds on the "due soon" table. A deliverable is what a project owes,
   so it is the filled chip; a milestone marks a date reached and is the outline
   one. Both clear WCAG AA on the row background, and they differ in fill as
   well as hue, so the distinction survives a monochrome screen. */
.badge--accent { background: var(--navy); border-color: var(--navy); color: #fff; }
.badge--outline { background: transparent; border-color: var(--muted); color: var(--muted); }
/* The third kind. A filled chip like the deliverable's, in the cyan the rest of
   the interface uses for "in hand", so it is never mistaken for either of the
   other two -- by fill against the milestone, by hue against the deliverable. */
.badge--todo { background: var(--cyan-text); border-color: var(--cyan-text); color: #fff; }

/* Role chips. Solid fills, one colour per role, so an Administrator is never
   mistaken for a Viewer at a glance; every pairing clears WCAG AA. */
.badge--administrator { background: var(--navy); border-color: var(--navy); color: #fff; }
.badge--editor { background: var(--cyan-text); border-color: var(--cyan-text); color: #fff; }
.badge--viewer { background: var(--sand); border-color: var(--sand-dark); color: #23201a; }

/* ------------------------------------------------------------ portraits

   A portrait is an aid to recognition beside a name, never the thing that
   carries it: every avatar in a list or a table has the name next to it, and
   the icon is marked decorative so a screen reader reads the person once.
   Images are square (480x480 as imported), but object-fit keeps a portrait of
   any shape from stretching. */

.avatar {
  flex: none; border-radius: 50%; object-fit: cover; object-position: 50% 25%;
  background: var(--sand-pale); border: 1px solid var(--border);
  /* The circle is a solid shape while the image loads, so a long list does not
     reflow line by line as the portraits arrive. */
  overflow: hidden;
}
.avatar--xs { width: 20px; height: 20px; }
.avatar--sm { width: 24px; height: 24px; }
.avatar--md { width: 40px; height: 40px; }
.avatar--lg { width: 160px; height: 160px; }

/* The stand-in for an account with no portrait: initials on the pale sand of
   the brand, in navy, which clears WCAG AA. */
.avatar--initials {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--heading-font); font-weight: 600; color: var(--navy);
  letter-spacing: .02em; user-select: none;
}
.avatar--xs.avatar--initials { font-size: .6rem; }
.avatar--sm.avatar--initials { font-size: .7rem; }
.avatar--md.avatar--initials { font-size: 1rem; }
.avatar--lg.avatar--initials { font-size: 3.4rem; }

/* Icon and name as one unit. inline-flex keeps them on the same line and
   vertically centred inside a table cell, a definition list or a sentence. */
.person {
  display: inline-flex; align-items: center; gap: .4rem; max-width: 100%;
}
.person__name { min-width: 0; }
/* A comma-separated run of contributors wraps as a list of chips rather than as
   one long line of text. */
td .person + .person { margin-left: .1rem; }
/* The same chips with no separator between them: the gap does the separating. */
.people-list { display: flex; flex-wrap: wrap; gap: .25rem .75rem; }
.people-list .person + .person { margin-left: 0; }
.cell--people { min-width: 14ch; }

/* The profile page header: the portrait leads, the name and role follow. */
.profile-header { display: flex; align-items: flex-start; gap: 1.25rem; }
.profile-header .avatar--lg { box-shadow: 0 1px 4px rgba(32, 41, 90, .18); }

@media (max-width: 640px) {
  .profile-header { gap: .9rem; }
  .profile-header .avatar--lg { width: 96px; height: 96px; }
  .avatar--lg.avatar--initials { font-size: 2rem; }
}

/* The signed-in user's own portrait in the header menu. */
.user-menu__trigger .avatar { margin-right: .1rem; }

/* ------------------------------------------------------------ progress */

.progress {
  position: relative; height: 1.1rem; min-width: 84px; background: var(--mist);
  border: 1px solid var(--border); border-radius: 999px; overflow: hidden;
}
.progress__bar { height: 100%; background: var(--cyan); }
.progress__label {
  position: absolute; inset: 0; font-size: .72rem; line-height: 1.1rem;
  text-align: center; color: #16202e;
}

/* --------------------------------------------------------------- forms */

.form { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; max-width: 900px; }

/* Some forms hold a wide table rather than a column of inputs: the intake
   pages with their proposed records, the team pages with their participants.
   A 900px column would leave those cramped beneath a full-width heading, so
   they take the whole page width instead. Their fields then flow into as many
   columns as the width allows, so the space is used rather than spent
   stretching one input across the screen. */
.form--wide { max-width: none; }
.form__fields {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 0 1.25rem; align-items: start;
}
.form__fields .field--wide { grid-column: 1 / -1; }

/* A value shown where a field would be, because it is already known and is not
   being asked for again. Sized like an input so the row still lines up. */
.value-static {
  margin: 0; padding: .45rem .55rem; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--mist); font-weight: 600;
}

/* The project months a document stated, shown beside the dates converted from
   them. Read-only by design: the document said M12, and the editable value is
   the date. */
.value-months { font-weight: 600; white-space: nowrap; }

.field { margin-bottom: .9rem; }
.field label { display: block; font-weight: 600; margin-bottom: .25rem; font-size: .9rem; }
/* Faded because UWMH takes no part in this record (PM-39), applied by
   static/js/uwmhinvolvement.js. Appearance only: the field stays clickable,
   tab-reachable and editable, so nothing here may disable it. Focusing
   anywhere inside brings it back to full strength, because a field being
   filled in has to be as readable as any other. */
.field--deemphasised { opacity: .55; }
.field--deemphasised label { color: var(--muted); }
.field--deemphasised:focus-within { opacity: 1; }
.field--deemphasised:focus-within label { color: inherit; }
.form-control, input[type="text"], input[type="search"], input[type="date"],
input[type="number"], input[type="email"], input[type="password"], select, textarea {
  width: 100%; padding: .45rem .55rem; border: 1px solid var(--border);
  border-radius: var(--radius); font: inherit; background: #fff; color: inherit;
}
.form-check { width: auto; }

/* Date field with its calendar button. The wrapper and button are injected by
   static/js/dateinput.js, so none of this applies until the enhancement runs --
   without JavaScript the input is an ordinary full-width text field. */
.date-field { position: relative; display: block; }
.date-field .form-control { padding-right: 2.4rem; }
.date-field__button {
  position: absolute; right: .3rem; top: 50%; transform: translateY(-50%);
  width: 1.8rem; height: 1.8rem; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid transparent; border-radius: 3px;
  font-size: 1rem; cursor: pointer;
}
.date-field__button:hover { background: var(--sand-pale); border-color: var(--sand); }
/* Rendered but invisible: showPicker() anchors its popup to this element and
   refuses to open on one that is display:none. */
.date-field__native {
  position: absolute; right: .8rem; bottom: .2rem;
  width: 1px; height: 1px; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}
/* Person picker. The wrapper and everything in it are injected by
   static/js/personselect.js, so without JavaScript the select is simply the
   full-width control it has always been. Every picker it handles chooses one
   person and becomes the combobox below. */
.person-field { display: block; }
.person-field--combo { position: relative; }
.person-field__input { padding-right: 1.7rem; }
.person-field__toggle {
  position: absolute; right: .45rem; top: 50%; transform: translateY(-50%);
  padding: 0; border: 0; background: none; line-height: 1;
  font-size: .7rem; color: var(--muted); cursor: pointer;
}
/* Positioned from script in viewport coordinates, so the popup is not clipped
   by the scrolling wrapper the team table sits in. */
.person-field__list {
  position: fixed; z-index: 60; margin: 0; padding: .2rem; list-style: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 6px 18px rgba(32, 41, 90, .18);
  overflow-y: auto;
}
.person-field__option { padding: .3rem .45rem; border-radius: 3px; cursor: pointer; }
.person-field__option[aria-selected="true"] { font-weight: 700; }
.person-field__option--blank { color: var(--muted); }
.person-field__option.is-active { background: var(--navy); color: #fff; }
.person-field__empty { padding: .3rem .45rem; color: var(--muted); font-size: .9rem; }
/* Rendered but invisible: a required control hidden with display:none leaves
   the browser nothing to anchor its validation message to, and Chrome then
   refuses to submit the form. */
.person-field__native {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
/* The team table's Person cell is the narrowest place these appear; keeping
   them tight there stops the row growing taller than its date fields. */
.table .person-field__input { padding: .25rem 1.5rem .25rem .35rem; font-size: .85rem; }
.table .person-field__list { font-size: .85rem; }
/* Compact multiple choice (PM-62). Injected by static/js/multiselect.js, so
   without JavaScript the select is the native multi-select. The button takes
   its padding, border and font from the plain controls beside it, so the
   filter row keeps one height. */
.multi-field { position: relative; display: block; }
.multi-field__toggle {
  position: relative; display: block; text-align: left; cursor: pointer;
  padding-right: 1.7rem; white-space: nowrap;
}
.multi-field__text { display: block; overflow: hidden; text-overflow: ellipsis; }
.multi-field__chevron {
  position: absolute; right: .45rem; top: 50%; transform: translateY(-50%);
  line-height: 1; font-size: .7rem; color: var(--muted);
}
.multi-field__list {
  position: fixed; z-index: 60; margin: 0; padding: .2rem;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 6px 18px rgba(32, 41, 90, .18);
  overflow-y: auto;
}
/* Scoped under .multi-field to outrank ".field label", which would otherwise
   make each row a bold caption. */
.multi-field .multi-field__option {
  display: flex; align-items: center; gap: .4rem; margin: 0;
  padding: .3rem .45rem; border-radius: 3px; cursor: pointer;
  font-size: inherit; font-weight: 400; white-space: nowrap;
}
.multi-field__option:hover, .multi-field__option:focus-within { background: var(--sand-pale); }
.multi-field__native {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
/* Single choice (PM-69). Injected by static/js/singleselect.js, so without
   JavaScript the select is the native one. The list is drawn in the page and
   placed from script, so it stays inside the browser window. */
.single-field { position: relative; display: block; }
.single-field__toggle {
  position: relative; display: block; text-align: left; cursor: pointer;
  padding-right: 1.7rem; white-space: nowrap;
}
.single-field__toggle:disabled { cursor: default; opacity: .6; }
.single-field__text { display: block; overflow: hidden; text-overflow: ellipsis; }
.single-field__chevron {
  position: absolute; right: .45rem; top: 50%; transform: translateY(-50%);
  line-height: 1; font-size: .7rem; color: var(--muted);
}
.single-field__list {
  position: fixed; z-index: 60; margin: 0; padding: .2rem; list-style: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 6px 18px rgba(32, 41, 90, .18);
  overflow-y: auto;
}
.single-field__option {
  padding: .3rem .45rem; border-radius: 3px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.single-field__option[aria-selected="true"] { font-weight: 700; }
.single-field__option--blank { color: var(--muted); }
.single-field__option[aria-disabled="true"] { color: var(--muted); cursor: default; }
.single-field__option.is-active { background: var(--navy); color: #fff; }
.single-field__native {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
textarea { resize: vertical; }
.field--error .form-control, .field--error input, .field--error select { border-color: var(--danger); }
/* The "showing N of M" line the linked association lists add for themselves.
   Reads as help text, but sits tight under its own list rather than under the
   field's own help paragraph. It is empty whenever nothing is hidden, so it
   takes no space until it has something to say. */
.linked-hint { margin: .25rem 0 0; }
.linked-hint:empty { display: none; }
.errors { list-style: none; margin: .25rem 0 0; padding: 0; color: var(--danger); font-size: .85rem; }
.required { color: var(--danger); }
.form__actions { display: flex; gap: .5rem; margin-top: 1rem; flex-wrap: wrap; }
.formset__actions { margin: .5rem 0 0; }
/* .button sets display:inline-block, which would otherwise beat the browser's
   own [hidden] rule and show the add-row button before JavaScript enables it. */
.button[hidden] { display: none; }
.field-extracted { background: var(--sand-pale); border-color: var(--sand); }

.filters {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .75rem; margin-bottom: 1rem;
}
.filters__row { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); align-items: end; }
/* Filters apply as they change, so this cell normally holds Reset alone; the
   Apply button only appears inside <noscript>. Wrapping keeps the pair tidy
   there and on a narrow screen. */
.field--actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* The Gantt People filter is a select2 box (PM-48). Its own stylesheet asks for
   a 20em minimum and the script writes the measured width inline, either of
   which would push it out of a cell of the grid above; the border matches the
   plain controls beside it. */
.filters .select2-container { min-width: 0; width: 100% !important; }
.filters .select2-container--default .select2-selection {
  border-color: var(--border); border-radius: var(--radius);
}

/* The dashboard's due-soon options are two short selects and a button beside a
   heading, not a filter panel of their own: no surface, and a natural width
   rather than one stretched across the row. Aligning on the end keeps the
   button on the same line as the labelled selects rather than riding above
   them. */
.filters--inline {
  background: none; border: 0; padding: 0; margin: 0;
  display: flex; gap: .75rem; align-items: end; flex-wrap: wrap;
}

/* The results are swapped in place when a filter changes. Dimming them while
   the new rows are in flight marks what is on screen as the previous filter's
   without moving anything, matching .gantt.is-busy. The named regions a page
   may swap alongside them (PM-44) dim on the same rule, so the whole page
   reads as one pending change rather than as several. */
.list-results.is-busy,
[data-list-region].is-busy { opacity: .55; transition: opacity .12s ease-in; }

/* The dashboard's content switch (PM-44): two links reading as one control, so
   the mode in force is visible without having to open anything. Links rather
   than buttons because each one is a bookmarkable address, and the selected
   side keeps its own colour rather than the link blue it would otherwise take.
   The legend matches the <label> of the selects beside it, which carry no class
   of their own. */
.field__legend { display: block; font-weight: 600; margin-bottom: .25rem; font-size: .9rem; }
.segmented {
  display: inline-flex; flex-wrap: wrap;
  border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
  background: var(--surface);
}
.segmented__option {
  padding: .35rem .7rem; font-size: .9rem; text-decoration: none;
  color: var(--muted); background: none; white-space: nowrap;
}
.segmented__option + .segmented__option { border-left: 1px solid var(--border); }
.segmented__option:hover { color: var(--text); background: var(--sand-pale); }
.segmented__option.is-selected {
  color: var(--text); font-weight: 600; background: var(--sand-pale);
}
/* Inside the rounded box, so the ring has to sit within the option rather than
   around it or the overflow above would clip it. */
.segmented__option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Selected-filter tags, injected by static/js/gantt.js: one chip per value in
   force. Badge sizing and colours, so they read as data rather than controls;
   they wrap instead of scrolling, and long labels are clipped with an ellipsis
   while the full text stays on the tooltip. */
.filter-tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .75rem; }
.filter-tags:empty, .filter-tags[hidden] { display: none; }
.filter-tag {
  display: inline-flex; align-items: center; gap: .15rem; max-width: 30ch;
  padding: .1rem .15rem .1rem .5rem; border-radius: 999px;
  border: 1px solid #a5d6ea; background: #e4f3fa; color: var(--cyan-text);
  font-size: .75rem; font-weight: 600;
}
.filter-tag__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-tag__remove {
  flex: none; width: 1.15rem; height: 1.15rem; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: 999px;
  background: none; color: inherit; font: inherit; cursor: pointer;
}
.filter-tag__remove:hover { background: #cbe7f4; border-color: currentColor; }
.filter-tag__remove:focus-visible { outline: 2px solid var(--cyan-text); outline-offset: 1px; }

.button {
  display: inline-block; padding: .45rem .95rem; border-radius: var(--radius);
  border: 1px solid var(--border); background: var(--surface); color: var(--navy);
  text-decoration: none; font-size: 1rem; font-weight: 600; cursor: pointer;
  white-space: nowrap;
}
.button:hover { background: var(--sand-pale); border-color: var(--sand); }
.button--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.button--primary:hover { background: var(--sand); border-color: var(--sand-dark); color: #23201a; }
.button--danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.button--danger:hover { background: #8b2020; color: #fff; }
.button--secondary { background: var(--mist); border-color: #d3d3cc; }
.button--link {
  background: none; border: none; color: var(--accent); padding: .2rem;
  text-decoration: underline; font-family: var(--body-font);
}
.button--link:hover { background: none; color: var(--cyan-text); }
.inline-form { display: inline-flex; gap: .5rem; align-items: center; }

/* Stepping between the records of one Project (PM-40). At either end of the
   sequence the control is a span rather than a link, so it cannot be clicked
   or tabbed to; aria-disabled is what says so to a screen reader, and the
   styling below only makes that visible. */
.record-nav { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.record-nav__position { font-size: .85rem; white-space: nowrap; }

/* Deliverable and Milestone detail pages keep their controls on a row of their
   own under the title, so a long title can never push them up beside it
   (PM-47). The actions sit at the right end of that row, and the negative top
   margin absorbs the bottom margin of the .page-header above so the row reads
   as part of the heading rather than as a second block. */
.record-toolbar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--gap); margin: -1rem 0 1rem;
}
.record-toolbar .actions { margin-left: auto; }
.button[aria-disabled="true"] {
  background: var(--mist); color: var(--muted); border-color: var(--border);
  cursor: default; opacity: .65;
}

/* ---------------------------------------------------- definitions/tree */

.definitions { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); margin: 0; }
.definitions dt { font-size: .78rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); }
.definitions dd { margin: .1rem 0 0; }

.link-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
.link-list li { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; row-gap: .25rem; }

.tree { list-style: none; margin: 0; padding: 0; }
.tree--nested { margin-left: 1.5rem; border-left: 2px solid var(--border); padding-left: .75rem; }
.tree__node { margin: .35rem 0; }
.tree__row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.tree__label { font-weight: 600; text-decoration: none; }
.tree__empty { margin: .2rem 0 .2rem 1.6rem; font-size: .85rem; }

.pagination { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; margin-top: .75rem; }
.pagination__status { color: var(--muted); font-size: .875rem; }

/* --------------------------------------------------------------- gantt */

.gantt-legend { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .8rem; color: var(--muted); margin-bottom: .5rem; }
.swatch { display: inline-block; width: .8rem; height: .8rem; border-radius: 2px; vertical-align: -1px; }
.swatch--planned { background: #cfe7f3; }
.swatch--actual { background: var(--navy); }
.swatch--progress { background: var(--cyan); }
.swatch--milestone { background: var(--sand-dark); transform: rotate(45deg); }
.swatch--deliverable { background: #1f7a4d; transform: rotate(45deg); }
.swatch--todo { background: var(--cyan-text); border-radius: 50%; }
.swatch--overdue { background: var(--danger); }
.swatch--today { background: rgba(165, 42, 42, .6); width: 2px; }

/* No side padding: the label column is frozen against the left-hand edge of
   this scroll container (position: sticky, below), and a padding gutter there
   would show the scrolled timeline running past the frozen names. Everything in
   here that is not the chart carries the inset itself.

   The panel scrolls in both directions and its height is bounded, so the
   sideways scroll bar sits along the bottom edge of the panel and stays in
   reach however many rows a fully expanded chart holds -- rather than at the
   foot of the whole chart, where a long portfolio would put it off screen. The
   cap is taken from the viewport so a tall screen shows more, with a floor so a
   short one is not left with a slot to read the chart through. */
.gantt {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .75rem 0; overflow: auto;
  max-height: max(26rem, calc(100vh - 16rem));
}
.gantt > .gantt__controls, .gantt > .empty-state, .gantt > .message {
  margin-left: .75rem; margin-right: .75rem;
}
/* Filters apply as they change, so the chart is swapped in place. Dimming it
   while the new data is in flight marks the rows on screen as the previous
   filter's without moving anything. */
.gantt.is-busy { opacity: .55; transition: opacity .12s ease-in; }

/* The dashboard's own chart (PM-43) shares the page with everything below it,
   so it is held to a shorter cap than the portfolio's own: it is one panel
   among several rather than the whole page. */
.gantt--embedded { max-height: 30rem; }

/* The legend belongs to the chart below it rather than to the heading above,
   so it sits nearer the chart than the panel header does. */
.gantt-section .gantt-legend { margin-top: .25rem; }

.gantt__controls {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: .5rem;
}
.gantt__summary { color: var(--muted); font-size: .85rem; margin: 0; }
.gantt__hidden-count { color: var(--muted); }
.gantt__toolbar { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
.gantt__toolbar .button { padding: .25rem .6rem; font-size: .8rem; }
.gantt__zoom { display: flex; gap: .25rem; }

/* One variable drives the label column so the rows, the header, the today
   overlay and the divider all line up. It is the stylesheet's own width until
   the reader drags the divider, which writes a pixel width onto this element.

   The timeline is as wide as the horizon needs: --month-count is set by
   static/js/gantt.js from the number of calendar months on the chart and
   --month-width by its zoom control, so every month keeps the same width and a
   long horizon scrolls sideways in .gantt rather than compressing the months.
   The default is narrow enough for about four years to be read at once.
   min-width keeps a short horizon filling the panel instead of huddling on the
   left. */
.gantt__chart {
  --label-width: 250px; --month-width: 28px;
  --band: #f7f7f4;
  width: calc(var(--label-width) + var(--month-count, 12) * var(--month-width));
  min-width: 100%; position: relative;
}
.gantt__body { position: relative; }

.gantt__row {
  display: grid; grid-template-columns: var(--label-width) 1fr;
  align-items: center; border-bottom: 1px solid #eef0f3;
}
/* The class selector would otherwise beat the browser's [hidden] rule. */
.gantt__row[hidden] { display: none; }

/* The month and year axis stays at the top of the panel while the rows scroll
   beneath it, so a date can always be read off a bar. Opaque, or the rows would
   show through it, and above the frozen item column (z-index 5) and the today
   overlay (4) so nothing is painted over it. It stops level with the resize
   layer (6), which is appended after it and therefore still draws the divider
   across it. */
.gantt__row--header {
  position: sticky; top: 0; z-index: 6;
  background: var(--surface);
  border-bottom: 2px solid var(--border); font-weight: 600; height: 2rem;
}

/* Alternating bands, one per top-level record -- normally a Project, with its
   work packages, tasks, deliverables and milestones. static/js/gantt.js tags
   every row with the group it belongs to and shades every other group, and a
   rule at each boundary says where one ends and the next begins.

   The frozen item column takes the band as its own opaque background rather
   than letting the row's show through: a translucent column would read the
   scrolling timeline through the names. */
.gantt__row.is-band-alt, .gantt__row.is-band-alt .gantt__label { background: var(--band); }
.gantt__row.is-group-start:not(:first-child) { border-top: 1px solid var(--border); }
/* Dimmed cell by cell rather than row by row: opacity on the row would make it
   a stacking context, and the frozen label column below could then no longer be
   painted over the today line that scrolls beneath it. */
.gantt__row.is-structural .gantt__label,
.gantt__row.is-structural .gantt__track { opacity: .55; }
.gantt__row--project .gantt__label { font-weight: 700; }

/* Frozen item column: the names stay against the left-hand edge while the
   timeline scrolls past them. Opaque, and above the today overlay (z-index 4),
   so no bar, tick or line shows through. */
.gantt__label {
  position: sticky; left: 0; z-index: 5;
  display: flex; align-items: center; gap: .3rem;
  padding: .3rem .5rem .3rem calc(.5rem + var(--level, 0) * 1.1rem);
  font-size: .85rem; white-space: nowrap; overflow: hidden;
  background: var(--surface); box-shadow: 1px 0 0 var(--border);
}
.gantt__link { text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
.gantt__type { color: var(--muted); font-size: .7rem; text-transform: capitalize; flex: none; }

/* Expand / collapse control on Project and Work Package rows.

   The two states carry different backgrounds so the shape of the tree is
   readable at a glance without tracing every glyph: an expanded row is filled
   with the sand accent, a collapsed one stays plain. The selector keys off
   aria-expanded, which the renderer already maintains, so the visual state and
   the state announced to a screen reader cannot drift apart. */
.gantt__toggle {
  flex: none; width: 1.15rem; height: 1.15rem; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--muted);
  border: 1px solid var(--border); border-radius: 3px;
  font-size: .7rem; cursor: pointer;
}
.gantt__toggle[aria-expanded="true"] {
  background: var(--sand); border-color: var(--sand-dark); color: #23201a;
}
.gantt__toggle:hover { background: var(--sand-pale); color: var(--navy); }
.gantt__toggle[aria-expanded="true"]:hover {
  background: var(--sand-dark); border-color: var(--sand-dark); color: #fff;
}
.gantt__toggle--none { visibility: hidden; cursor: default; }

.gantt__track { position: relative; height: 1.7rem; }
.gantt__axis { position: absolute; inset: 0; }
/* One tick per calendar month: the border draws the month's left-hand edge and
   the tick's own width is that month's column. The closing tick sits on the
   right edge of the last month and carries no label.

   Only some ticks are labelled once the months are narrow -- see the zoom
   levels in static/js/gantt.js -- and a labelled one is drawn a shade stronger
   so the axis still reads as a grid. The label runs past its own column rather
   than being clipped by it: at every zoom level the labelled ticks are spaced
   far enough apart for the whole of "Jan24" to fit between them.

   Running past its own column means running across the unlabelled months in
   between, so the label carries its own opaque backing and sits above the ticks
   -- otherwise a month line would be drawn straight through the text. The side
   padding keeps the label clear of the two lines nearest it, including its own,
   which stays visible because the backing begins inside the tick's border. */
.gantt__tick { position: absolute; top: 0; bottom: 0; border-left: 1px solid var(--border); }
.gantt__tick--labelled { border-left-color: #c8cdd4; }
.gantt__tick--end { width: 0; }
.gantt__tick-label {
  position: relative; z-index: 1;
  display: block; width: max-content;
  font-size: .68rem; color: var(--muted); padding: 0 .3rem;
  white-space: nowrap; background: var(--surface);
}

/* Today marker.
   The line must be measured against the timeline column only, so the overlay
   repeats the row grid and puts an empty gutter under the label column.
   Positioning it against the whole chart would place it at the wrong date. */
.gantt__today-layer {
  position: absolute; inset: 0; z-index: 4; pointer-events: none;
  display: grid; grid-template-columns: var(--label-width) 1fr;
}
.gantt__today-gutter { /* spacer beneath the label column */ }
.gantt__today-track { position: relative; }
.gantt__today {
  position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
  background: rgba(165, 42, 42, .6);
}
/* Just below the axis, which is opaque and sticky and would otherwise hide the
   badge: 2rem is .gantt__row--header's own height. The line above that point is
   hidden behind the axis too, which leaves the month labels legible. */
.gantt__today-label {
  position: absolute; top: 2rem; transform: translateX(-50%);
  background: var(--danger); color: #fff; font-size: .65rem; font-weight: 600;
  padding: 0 .3rem; border-radius: 0 0 3px 3px; white-space: nowrap;
}

/* The divider between the frozen item column and the timeline.

   It sits in a layer of its own so it can run the whole height of the chart
   without belonging to any one row. The layer's padding puts it on the column
   boundary and the sticky offset repeats that measurement, so the divider is
   held on the boundary as the timeline scrolls under it. The layer itself lets
   every click through; only the divider takes one. */
.gantt__resize-layer {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  padding-left: calc(var(--label-width) - 3px);
}
.gantt__divider {
  position: sticky; left: calc(var(--label-width) - 3px);
  width: 6px; height: 100%; pointer-events: auto;
  cursor: col-resize; touch-action: none;
}
.gantt__divider::before {
  content: ""; display: block; width: 2px; height: 100%; margin: 0 auto;
  background: var(--border);
}
.gantt__divider:hover::before, .gantt__divider:focus-visible::before,
.gantt__divider.is-dragging::before { background: var(--cyan); }
.gantt__divider:focus-visible { outline: 2px solid var(--cyan); outline-offset: 0; }

/* min-width keeps a short item visible on a long horizon, where one day is
   less than a pixel wide. */
.gantt__bar { position: absolute; top: .35rem; height: .7rem; min-width: 3px; border-radius: 3px; overflow: hidden; }
.gantt__bar--planned { background: #cfe7f3; border: 1px solid #96c6dd; }
.gantt__bar--planned.is-overdue { border-color: var(--danger); }
.gantt__bar--actual { top: 1.15rem; height: .35rem; background: var(--navy); border-radius: 2px; }
.gantt__progress { height: 100%; background: var(--cyan); }

.gantt__marker {
  position: absolute; top: .45rem; width: .65rem; height: .65rem;
  transform: translateX(-50%) rotate(45deg);
}
.gantt__marker--milestone { background: var(--sand-dark); }
.gantt__marker--deliverable { background: #1f7a4d; }
/* A round marker rather than a diamond: a ToDo is not a project artefact,
   and the shape says so before the colour does. */
.gantt__marker--todo { background: var(--cyan-text); border-radius: 50%; transform: translateX(-50%); }
.gantt__marker.is-overdue { background: var(--danger); }
.gantt__nodate { font-size: .72rem; color: var(--muted); padding-left: .3rem; }

/* --month-width belongs to the zoom control, which writes it onto the element
   itself, so only the item column is narrowed here -- and only until the reader
   drags the divider, whose own width is written onto the element too. */
@media (max-width: 700px) {
  .gantt__chart { --label-width: 170px; }
}
