/* ============================================================================
   mpdg timeline (Chart view) — Phase 1.

   Visual language is deliberately the CALENDAR's, not a chart widget's. Bars use
   foundation's shared scheduling-block grammar (--mp-block-*), so a timeline bar
   and a calendar session block are the same material: 8px radius, a 22% accent
   fill over --panel, a 55% hairline, and a 3px left accent rail. Colours come
   from foundation tokens via --mpt-acc; nothing here hardcodes a hex.
   ============================================================================ */

.mpt{
  --mpt-label-w:240px;
  --mpt-row-h:40px;
  --mpt-group-h:44px;
  /* Printable canvas width. Read by draw() to fit the whole domain onto the page, so the
     print scale is tunable here rather than hardcoded in JS. A4 landscape at 10mm margins
     is ~277mm, which is ~1047px at 96dpi. */
  --mpt-print-w:1040px;
  display:none;
  border:1px solid var(--border);
  border-radius:14px;
  background:color-mix(in srgb,var(--panel-solid) 40%,transparent);
  overflow:hidden;
}
.mpdg[data-mpdg-active-view="chart"] .mpt{display:block;}

/* ===== Filling a pane that does not scroll =====
   When the grid root carries the shell's .mp-page-fill contract, foundation makes the pane a flex
   column and the grid the item that takes what is left. From there down it is this file's job:
   the card becomes a column of its own so the toolbar keeps its natural height and the scroller
   takes the remainder.

   This REPLACED a measured fit in JS. Timeline.fitViewport() aimed the scroller at the bottom of
   the sidebar's user block and wrote a pixel height into --mpt-scroll-h — a datagrid reaching into
   the shell to find out how tall it was allowed to be. It could not be right: below the point it
   aimed at, the pane still carried the grid's bottom margin and its own bottom padding, so every
   screen scrolled by the difference. Height is not a number to compute here; it is what is left,
   and only the box model knows that.

   min-height:0 on both, because a flex item will not shrink below its content by default and the
   overflow would go straight back to the pane. */
.content > .mpdg.mp-page-fill[data-mpdg-active-view="chart"] .mpt{
  display:flex;
  flex-direction:column;
  min-height:0;
}
.content > .mpdg.mp-page-fill[data-mpdg-active-view="chart"] .mpt-scroll{
  flex:1 1 auto;
  min-height:0;
  /* Beats the fallback below without !important: this selector is simply more specific. */
  height:auto;
}
.mpdg[data-mpdg-active-view="chart"] .mpdg-list{display:none;}
.mpdg[data-mpdg-active-view="chart"] .mpdg-board{display:none;}
.mpdg[data-mpdg-active-view="chart"] .mpdg-foot{display:none;}
.mpdg[data-mpdg-active-view="chart"] [data-mpdg-hide-in-chart]{display:none;}
/* When this engine is mounted, the legacy percent-painter container must not also
   claim the chart slot — data-grid.css unhides it by attribute, so it would render
   as an empty bordered box under the timeline. */
.mpdg[data-mpdg-active-view="chart"]:has([data-mpdg-timeline]) .mpdg-chart{display:none;}

/* ---------- toolbar ---------- */
.mpt-toolbar{
  display:flex;align-items:center;gap:8px;
  /* Wraps because the bar now carries BOTH the period pills and the pager. A second line on
     a narrow pane is honest; squeezing eleven controls onto one is how a toolbar starts
     truncating labels nobody can then read. */
  flex-wrap:wrap;row-gap:6px;
  padding:8px 10px;
  border-bottom:1px solid var(--border);
}
.mpt-spacer{flex:1 1 auto;}

/* The period pager: the shared .mp-seg-shell tray (actions-badges), same as the Calendar's
   Prev / Today / Next and the period pills beside it. NO LAYOUT IS DECLARED HERE: the tray
   owns the row, the gutters and the inset, and the segments own their height. What used to
   sit here — display:flex with its own gap — was the start of the per-instance treatment the
   seg vocabulary exists to have retired, and it disagreed with the tray by one pixel.

   THE DISABLED RULE IS GONE FOR THE SAME REASON. `.mpt-pager .is-disabled{opacity:.4}` was a
   second copy of the registry's `.btn.is-disabled{opacity:.5}` carrying a different number,
   and winning on specificity. The arrows set `disabled` and the registry paints it. */
.mpt-pager{flex:0 0 auto;}
/* The one thing that IS this consumer's business: a period label changes width as it counts
   (Q1 2026 → December 2026), and a tray that resizes under the pointer is a tray you cannot
   click twice. Tabular figures and a floor, so paging moves the label and nothing else. */
.mpt-pager-label{min-width:118px;font-variant-numeric:tabular-nums;}

/* Empty-month signpost — shown in the body when the paged month holds no dated work, so a
   dead lane becomes a pointer to the nearest work either side. */
.mpt-empty-period{
  padding:30px 20px;text-align:center;color:var(--muted);
}
.mpt-empty-period-msg{margin-bottom:12px;font-style:italic;}
.mpt-empty-period-nav{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
/* No zoom/action button rules here by design: the toolbar is built through the Buttons
   registry (window.mpButton) into the shared .mp-seg-shell tray, so its paint lives with
   every other button on the platform. A local rule here would be the start of the
   per-instance switcher treatment the seg vocabulary exists to have retired. */

/* ---------- scroll shell ----------
   One scroller drives both axes. The label column sticks left and the header
   sticks top inside it, so the two never drift apart the way separately
   scrolled panes do. */
/* Height is what the box model leaves — see the fill rules above, which is where a host that
   opts into the shell contract gets it. This vh value is the FALLBACK for a host that does not:
   a mount nested somewhere the pane's :has(> .mp-page-fill) cannot reach still gets a usable
   scroller rather than collapsing to nothing. It is a cap and it is honest about being one; it
   cannot stretch a short chart to the bottom, which is exactly why the fill path exists.
   --mpt-scroll-h is no longer written by anything and is kept only so a host may set it. */
.mpt-scroll{overflow:auto;height:var(--mpt-scroll-h,70vh);position:relative;}
.mpt-grid{display:block;width:max-content;min-width:100%;}

.mpt-head{
  display:flex;position:sticky;top:0;z-index:6;
  background:var(--panel-solid);
  border-bottom:1px solid var(--border);
}
.mpt-head-label{
  position:sticky;left:0;z-index:7;
  flex:0 0 var(--mpt-label-w);width:var(--mpt-label-w);
  box-sizing:border-box;
  padding:8px 12px;
  background:var(--panel-solid);
  border-right:1px solid var(--border);
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-heavy,800);
  letter-spacing:var(--mp-ls-wide,.04em);text-transform:uppercase;
  color:var(--muted);
}
.mpt-bands{position:relative;flex:1 0 auto;}
.mpt-band{position:relative;height:22px;}
.mpt-band-primary{border-bottom:1px solid color-mix(in srgb,var(--border) 60%,transparent);}
.mpt-band-cell{
  position:absolute;top:0;bottom:0;box-sizing:border-box;
  border-left:1px solid var(--border);
  display:flex;align-items:center;
  overflow:hidden;white-space:nowrap;
  padding:0 6px;
}
.mpt-band-primary .mpt-band-cell{
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-heavy,800);
  letter-spacing:var(--mp-ls-wide,.04em);text-transform:uppercase;
  color:var(--muted);
}
.mpt-band-secondary .mpt-band-cell{
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-medium,500);
  color:color-mix(in srgb,var(--muted) 80%,transparent);
  justify-content:center;padding:0 2px;
}
.mpt-tick-day.is-weekend span{opacity:.45;}

/* ---------- body ---------- */
.mpt-body{position:relative;}
/* Virtualisation: the two spacers stand in for the rows that are not rendered, so the
   scrollbar reflects the whole timeline while the DOM only ever holds the visible window
   plus overscan. They must not collapse or the scroll height lies. */
.mpt-spacer-top,.mpt-spacer-bottom{flex:none;pointer-events:none;}
.mpt-window{display:block;}
.mpt-overlay{
  position:absolute;top:0;bottom:0;left:var(--mpt-label-w);
  pointer-events:none;z-index:0;
}
.mpt-weekend{position:absolute;top:0;bottom:0;background:color-mix(in srgb,var(--muted) 7%,transparent);}
/* Today: the calendar's neon bar, turned on its side. Same token, same glow. */
.mpt-today{
  position:absolute;top:0;bottom:0;width:2px;
  background:var(--highlight);
  box-shadow:0 0 8px color-mix(in srgb,var(--highlight) 60%,transparent),0 0 3px var(--highlight);
  z-index:2;
}

/* No z-index: a stacking context here would trap the labels and bars inside the row,
   making it impossible to order them against the connector layer, which is a sibling. */
.mpt-row{display:flex;align-items:stretch;min-height:var(--mpt-row-h);position:relative;}
/* Separator on EVERY row, not only siblings: with virtualisation the first rendered row
   changes as you scroll, and a height that depends on position makes the offset model
   drift a pixel per row. Uniform rows keep the arithmetic honest. */
.mpt-row{border-bottom:1px solid color-mix(in srgb,var(--border) 55%,transparent);}
.mpt-row-group{min-height:var(--mpt-group-h);background:color-mix(in srgb,var(--muted) 6%,transparent);}
.mpt-row-task:hover{background:color-mix(in srgb,var(--text) 4%,transparent);}
/* While a drag is reordering rows the moved bar must stay legible above its neighbours. */
body.mp-dragging .mpt-bar.is-dragging{z-index:7;}
/* Insertion boundary: where the dragged row lands when you let go. Rows do not move
   during the drag — a list that re-sorts under a live pointer takes the target out from
   under the hand aiming at it — so the destination is promised by this line instead.
   Brand gradient, not --highlight: --highlight is already the today marker and the
   focus ring, and a third meaning on the same colour is a colour that means nothing.
   Above the sticky label (z-index 4) so the boundary reads across the full row, and
   below the dragged bar (z-index 7) so it never covers what you are holding. */
.mpt-slot{
  position:absolute;left:0;right:0;height:2px;margin-top:-1px;
  z-index:5;pointer-events:none;
  background:var(--grad-brand);border-radius:2px;
  opacity:0;transition:opacity .12s ease,top .14s ease;
}
.mpt-slot.is-on{opacity:1;}
/* The boundary is real but lies past the visible window. It is pinned to the edge it lies
   beyond and broken into dashes, so it reads as a DIRECTION rather than a place: a solid
   line hard against the viewport edge would claim a slot that is not there. The canvas is
   deliberately not scrolled to it - rows hold still during a drag so the bar never leaves
   the pointer, and moving them would trade one broken promise for another. */
.mpt-slot.is-offscreen{
  height:3px;margin-top:-2px;opacity:.85;
  -webkit-mask:repeating-linear-gradient(90deg,#000 0 14px,transparent 14px 24px);
  mask:repeating-linear-gradient(90deg,#000 0 14px,transparent 14px 24px);
}
@media (prefers-reduced-motion:reduce){
  .mpt-row{transition:none !important;}
  .mpt-slot{transition:none !important;}
}

.mpt-label{
  position:sticky;left:0;z-index:4;
  flex:0 0 var(--mpt-label-w);width:var(--mpt-label-w);
  box-sizing:border-box;
  display:flex;align-items:center;gap:6px;
  padding:6px 12px;
  background:var(--panel-solid);
  border-right:1px solid var(--border);
  min-width:0;
}
.mpt-row-group > .mpt-label{background:color-mix(in srgb,var(--muted) 6%,var(--panel-solid));}
.mpt-label-child{padding-left:30px;}
.mpt-label-text{
  flex:1 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:var(--mp-fs-sm,12px);color:var(--muted);
}
.mpt-row-group .mpt-label-text{
  font-size:var(--mp-fs-base,13px);font-weight:var(--mp-fw-heavy,800);color:var(--text);
}
.mpt-count{
  flex:none;
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);
  color:var(--muted);
  background:color-mix(in srgb,var(--muted) 16%,transparent);
  border-radius:999px;padding:1px 6px;
}
.mpt-twist{
  flex:none;appearance:none;border:0;background:transparent;cursor:pointer;
  width:18px;height:18px;padding:0;display:grid;place-items:center;color:var(--muted);
}
.mpt-twist:focus-visible{outline:2px solid var(--highlight);outline-offset:1px;border-radius:4px;}

.mpt-lane{position:relative;flex:1 0 var(--mpt-lane-w);width:var(--mpt-lane-w);min-height:inherit;}

/* ---------- bars ----------
   The shared block recipe. Identical composition to .mpcal-block so a bar and a
   calendar block read as the same object seen on a different axis. */
.mpt-bar{
  position:absolute;top:50%;transform:translateY(-50%);
  height:30px;box-sizing:border-box;
  padding:0;
  /* overflow VISIBLE: the knobs and the link handle straddle the edges and would
     otherwise be clipped away and unclickable. Content clipping moved to
     .mpt-bar-clip, mirroring .mpcal-block / .mpcal-block-clip. */
  appearance:none;cursor:pointer;overflow:visible;z-index:3;
  border:0;border-left:var(--mp-block-rail,3px) solid var(--mpt-acc);
  border-radius:var(--mp-block-radius,8px);
  background:color-mix(in srgb,var(--mpt-acc) var(--mp-block-fill,22%),var(--panel-solid));
  box-shadow:inset 0 0 0 var(--mp-block-hair,.5px) color-mix(in srgb,var(--mpt-acc) var(--mp-block-border,55%),transparent);
  color:var(--text);
  transition:filter .12s ease,box-shadow .12s ease;
}
.mpt-bar:hover{filter:brightness(1.08);}
.mpt-bar:focus-visible{outline:2px solid var(--mpt-acc);outline-offset:2px;}
.mpt-bar.is-locked{cursor:default;opacity:.72;}
/* A legacy-scheduled bar is drawn from the retiring task columns rather than from
   sessions; the dashed rail marks it so the provenance is visible, not guessed. */
.mpt-bar.is-legacy{border-left-style:dashed;}
.mpt-bar-clip{
  position:absolute;inset:0;overflow:hidden;
  border-radius:inherit;
  display:flex;align-items:center;
  padding:0 7px;
  pointer-events:none;
}
/* Booked: how much of the needed work is actually in someone's calendar. Sits behind
   the logged fill, so a window can read as 'committed but not started'. */
.mpt-bar-booked{
  position:absolute;left:0;top:0;bottom:0;
  background:color-mix(in srgb,var(--mpt-acc) 18%,transparent);
  pointer-events:none;
}
.mpt-bar-fill{
  position:absolute;left:0;top:0;bottom:0;
  background:color-mix(in srgb,var(--mpt-acc) 34%,transparent);
  pointer-events:none;
}
.mpt-bar-text{
  position:relative;
  font-size:var(--mp-fs-sm,12px);font-weight:var(--mp-fw-semibold,600);
  line-height:var(--mp-lh-none,1);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}

.mpt-milestone{
  position:absolute;top:50%;z-index:3;
  width:14px;height:14px;margin-left:-7px;
  transform:translateY(-50%) rotate(45deg);
  appearance:none;border:0;padding:0;cursor:pointer;
  background:var(--mpt-acc);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--panel-solid) 92%,transparent);
  border-radius:2px;
}
.mpt-milestone:hover{filter:brightness(1.12);}
.mpt-milestone:focus-visible{outline:2px solid var(--mpt-acc);outline-offset:3px;}

/* Project roll-up: the thin outline is the committed plan, the solid bar is what
   the tasks actually add up to. Divergence between them IS the signal. */
.mpt-plan{
  position:absolute;top:7px;height:8px;border-radius:4px;z-index:3;
  border:1px dashed color-mix(in srgb,var(--mpt-acc) 55%,transparent);
  background:transparent;
}
.mpt-rollup{
  position:absolute;bottom:8px;height:12px;border-radius:5px;z-index:3;
  background:color-mix(in srgb,var(--mpt-acc) 40%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--mpt-acc) 65%,transparent);
}
.mpt-nodate{
  position:absolute;left:8px;top:50%;transform:translateY(-50%);
  font-size:var(--mp-fs-2xs,10px);font-style:italic;color:var(--muted);opacity:.7;
}

/* ---------- capacity lane ----------
   Same px-per-day scale as the timeline above, so a column of red sits directly under the
   bars that caused it. */
/* Pinned to the bottom of the scrollport, so the load stays in view while you scroll the
   plan above it — the point of the lane is to be read AGAINST the bars, not after them.
   Sticky rather than moved outside the scroller, because it must keep scrolling
   horizontally with the timeline or a column would no longer sit under its own day. */
.mpt-cap{
  position:sticky;bottom:0;z-index:5;
  border-top:1px solid var(--border);
  background:var(--panel-solid);
}
/* Sticky left: the lane scrolls horizontally with the timeline, so its header would
   otherwise slide off the screen. */
.mpt-cap-head{
  position:sticky;left:0;z-index:4;
  width:var(--mpt-label-w);min-width:var(--mpt-label-w);
  appearance:none;border:0;background:var(--panel-solid);cursor:pointer;
  display:flex;align-items:center;gap:8px;padding:9px 12px;
  color:var(--muted);
  font-size:var(--mp-fs-xs,11px);font-weight:var(--mp-fw-heavy,800);
  letter-spacing:var(--mp-ls-wide,.04em);text-transform:uppercase;
}
.mpt-cap-head:hover{color:var(--text);}
.mpt-cap-head:focus-visible{outline:2px solid var(--highlight);outline-offset:-2px;}
.mpt-cap-title{flex:0 0 auto;}
.mpt-count.is-over{
  color:var(--black,#06111d);
  background:var(--amber);
}
.mpt-cap-body{display:none;}
.mpt-cap.is-open .mpt-cap-body{display:block;}
.mpt-cap-row{display:flex;align-items:stretch;min-height:34px;background:var(--panel-solid);}
.mpt-cap-row + .mpt-cap-row{border-top:1px solid color-mix(in srgb,var(--border) 55%,transparent);}
.mpt-cap-label{gap:6px;}
.mpt-cap-contract{
  flex:none;
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);
  color:var(--muted);
  background:color-mix(in srgb,var(--muted) 12%,transparent);
  border-radius:999px;padding:1px 6px;
}
.mpt-cap-lane{position:relative;flex:1 0 var(--mpt-lane-w);width:var(--mpt-lane-w);min-height:34px;}
.mpt-cap-bar{
  position:absolute;bottom:0;
  background:color-mix(in srgb,var(--blue) 55%,transparent);
  border-radius:2px 2px 0 0;
}
/* Over the contracted day, and well over it. Amber warns, red states. */
.mpt-cap-bar.is-over{background:color-mix(in srgb,var(--amber) 70%,transparent);}
.mpt-cap-bar.is-blown{background:color-mix(in srgb,var(--red) 72%,transparent);}
/* History is shown, not shouted about: a day that has already happened cannot be
   rescheduled, so it keeps its shape and loses its alarm. */
.mpt-cap-bar.is-past{background:color-mix(in srgb,var(--muted) 32%,transparent);}
.mpt-cap-bar.is-past.is-over,
.mpt-cap-bar.is-past.is-blown{background:color-mix(in srgb,var(--muted) 52%,transparent);}
/* The full-day line: makes "over" readable without a tooltip. */
.mpt-cap-full{
  position:absolute;left:0;right:0;top:0;height:1px;
  background:color-mix(in srgb,var(--muted) 45%,transparent);
  pointer-events:none;
}

/* Unplanned work, per project.
   The count sits on the project row and the strip opens inside that project's own
   section, a short drag from the lane the work will land in. Row-shaped like every
   other row so the label column and the lane stay in one grid. */
.mpt{--mpt-unplanned-h:44px;}

.mpt-unplanned-toggle{
  flex:none;margin-left:auto;appearance:none;cursor:pointer;
  border:1px dashed var(--mp-slate-18,rgba(148,163,184,.18));border-radius:999px;
  background:transparent;color:var(--muted);
  padding:1px 8px;font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);
  white-space:nowrap;
}
.mpt-unplanned-toggle:hover{border-color:var(--mp-slate-35,rgba(148,163,184,.35));color:var(--text);}
.mpt-unplanned-toggle.is-on{
  border-style:solid;border-color:color-mix(in srgb,var(--highlight) 45%,transparent);
  background:color-mix(in srgb,var(--highlight) 12%,transparent);color:var(--highlight);
}

.mpt-unplanned{min-height:var(--mpt-unplanned-h);background:var(--mp-fill-035,rgba(148,163,184,.035));}
/* Lit while a bar is over it, so the gesture says what the drop will do before it does
   it. Only ever lit for a bar the server said may be unplanned. */
.mpt-unplanned.is-drop-target{
  background:color-mix(in srgb,var(--amber) 12%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--amber) 45%,transparent);
}
.mpt-unplanned-label{color:var(--muted);font-size:var(--mp-fs-2xs,10px);}
/* Sticky beside the label column, like the labels themselves. The lane scrolls with the
   canvas, and a backlog that scrolls away with August is a backlog you cannot reach —
   the chips have to stay put while the dates move under them. */
/* Two classes: .mpt-lane sets position:relative later in this file, and a single-class
   override of it is a coin toss on source order. */
.mpt-unplanned .mpt-unplanned-lane{
  position:sticky;left:var(--mpt-label-w);z-index:3;
  /* Content width, not lane width: a sticky box as wide as the canvas has nowhere to
     slide, so it scrolls away with the dates like anything else. */
  flex:0 0 auto;width:auto;min-width:0;
  display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:6px 10px;
}

/* The chip being dragged stays in place, dimmed: removing it would reflow the strip
   under the pointer mid-gesture. */
.mpt-chip.is-draggable{cursor:grab;}
.mpt-chip.is-draggable:active{cursor:grabbing;}
.mpt-chip.is-dragging{opacity:.4;}
.mpt-chip.is-readonly{opacity:.6;cursor:default;}

/* Follows the pointer carrying the landing window — and turns amber the moment that
   window would contradict a finish-to-start order. */
.mpt-plan-ghost{
  position:fixed;z-index:60;pointer-events:none;
  transform:translate(12px,12px);
  padding:4px 9px;border-radius:6px;
  border:1px solid color-mix(in srgb,var(--highlight) 45%,transparent);
  background:var(--panel-solid,#0f1a26);color:var(--text);
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);white-space:nowrap;
  box-shadow:0 8px 20px var(--mp-sh-40b,rgba(0,0,0,.4));
}
.mpt-plan-ghost.is-breaking{
  border-color:color-mix(in srgb,var(--amber) 55%,transparent);
  color:var(--amber);
}

.mpt-chip{
  display:inline-flex;align-items:center;
  max-width:220px;margin:0 6px 6px 0;padding:3px 9px;
  border-radius:var(--mp-block-radius,8px);
  border-left:var(--mp-block-rail,3px) solid var(--mpt-acc);
  background:color-mix(in srgb,var(--mpt-acc) var(--mp-block-fill,22%),var(--panel-solid));
  box-shadow:inset 0 0 0 var(--mp-block-hair,.5px) color-mix(in srgb,var(--mpt-acc) var(--mp-block-border,55%),transparent);
  font-size:var(--mp-fs-2xs,10px);color:var(--text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  cursor:default;
}

.mpt-empty{padding:34px 20px;text-align:center;color:var(--muted);font-style:italic;}

/* ---------- direct manipulation ----------
   The knob is foundation's shared affordance (--mp-knob-*), the same object the
   calendar puts on its session blocks: hidden at rest, revealed on hover, with a
   hit area far larger than the dot so the edge is easy to grab. */
/* Unplan. Edge furniture like the knobs: hidden at rest, revealed with the bar, so a
   chart at a glance is bars and nothing else. It exists only when the server said the
   task may be unplanned, so it is never a control that fails. */
.mpt-unplan{
  position:absolute;top:-7px;right:-7px;
  width:16px;height:16px;border-radius:50%;
  border:1px solid var(--border);background:var(--panel-solid,#0f1a26);
  cursor:pointer;opacity:0;transition:opacity .12s ease;z-index:5;
}
.mpt-unplan{display:inline-flex;align-items:center;justify-content:center;}
.mpt-unplan .mp-icon,.mpt-unplan svg{width:10px;height:10px;color:var(--muted);}
.mpt-bar:hover .mpt-unplan,.mpt-bar:focus-visible .mpt-unplan{opacity:1;}
.mpt-unplan:hover{border-color:color-mix(in srgb,var(--amber) 55%,transparent);}
.mpt-unplan:hover .mp-icon,.mpt-unplan:hover svg{color:var(--amber);}

.mpt-knob{
  position:absolute;top:0;bottom:0;
  width:var(--mp-knob-hit,14px);
  cursor:ew-resize;z-index:3;
}
.mpt-knob-start{left:calc(var(--mp-knob-hit,14px) / -2);}
.mpt-knob-end{right:calc(var(--mp-knob-hit,14px) / -2);}
.mpt-knob::before{
  content:"";position:absolute;top:50%;left:50%;
  width:var(--mp-knob-size,8px);height:var(--mp-knob-size,8px);
  margin:calc(var(--mp-knob-size,8px) / -2) 0 0 calc(var(--mp-knob-size,8px) / -2);
  border-radius:50%;
  background:var(--mpt-acc);
  border:var(--mp-knob-ring,2px) solid var(--mp-fill-92,rgba(255,255,255,.92));
  box-shadow:0 1px 4px var(--mp-sh-55,rgba(0,0,0,.55));
  opacity:0;transform:scale(.55);
  transition:opacity .12s ease,transform .12s ease;
}
.mpt-bar:hover .mpt-knob::before,
.mpt-bar:focus-visible .mpt-knob::before{opacity:1;transform:scale(1);}
/* While any drag is in flight, every other bar's knobs stand down so the canvas
   is not speckled with affordances that are not in play. */
body.mp-dragging .mpt-bar:not(.is-dragging) .mpt-knob::before{opacity:0 !important;}

.mpt-bar:not(.is-locked){cursor:grab;}
body.mp-dragging .mpt-bar.is-dragging{cursor:grabbing;}
.mpt-bar.is-dragging{
  z-index:5;
  box-shadow:inset 0 0 0 1px var(--mpt-acc),0 6px 18px var(--mp-sh-40b,rgba(0,0,0,.4));
  filter:brightness(1.1);
}
.mpt-plan.is-draggable{cursor:grab;pointer-events:auto;}
.mpt-plan.is-dragging{border-style:solid;box-shadow:0 4px 12px var(--mp-sh-40b,rgba(0,0,0,.4));}

/* Global cursor + selection lock for the duration of a drag, matching the
   calendar's body.mpcal-dragging behaviour. */
body.mp-dragging{cursor:grabbing !important;}
body.mp-dragging.mp-dragging-ew{cursor:ew-resize !important;}
body.mp-dragging,body.mp-dragging *{
  -webkit-user-select:none !important;user-select:none !important;
}

/* Live read-out of the landing dates. Anchored to the bar, never to the cursor,
   so it does not obscure the row you are aiming at. */
.mpt-hint{
  position:absolute;z-index:8;pointer-events:none;
  padding:3px 8px;border-radius:6px;
  background:var(--panel-solid);
  border:1px solid var(--border);
  box-shadow:0 6px 18px var(--mp-sh-40b,rgba(0,0,0,.4));
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);
  color:var(--text);white-space:nowrap;
  opacity:0;transition:opacity .1s ease;
}
.mpt-hint.is-on{opacity:1;}
/* A move that would put an order out of sequence says so BEFORE you let go. Every other
   timeline recalculates afterwards and leaves you to find the damage. */
.mpt-hint.is-breaking{
  border-color:color-mix(in srgb,var(--amber) 55%,transparent);
  background:color-mix(in srgb,var(--amber) 10%,var(--panel-solid));
}
.mpt-hint-when{display:block;}
.mpt-hint-warn{
  display:block;margin-top:2px;
  color:var(--amber);font-weight:var(--mp-fw-heavy,800);
}
/* The connectors that would break, marked while the drag is still in the air. */
.mpt-link.is-breaking{
  stroke:var(--amber) !important;stroke-width:2.5 !important;
  stroke-dasharray:none !important;opacity:1 !important;
}
.mpt-link-head.is-breaking{fill:var(--amber) !important;opacity:1 !important;}
.mpt-link-stub.is-breaking{stroke:var(--amber) !important;opacity:1 !important;}

/* ---------- dependency connectors ----------
   An SVG overlay above the weekend/today layer but below the bars, so a link never
   steals a pointer from the thing it connects. Only the fat invisible hit path is
   interactive. */
/* Origin is the BODY's left edge, not the lane's. Connector coordinates come from
   getBoundingClientRect() measured against .mpt-body, which includes the sticky label
   column — offsetting the layer by the label width drew every line exactly one label
   width to the right of the bars it connects. The sticky labels have a higher z-index,
   so they still paint over anything that reaches into their column. */
/* BELOW the rows, deliberately.
   A clip-path used to keep this layer out of the label column, but the clip lives in the
   SVG's own coordinate space while the label column is position:sticky — scroll right and
   the two stop lining up, so connectors drew straight across the labels. Painting under
   the rows solves it structurally at any scroll offset: the labels are opaque and win, and
   a bar wins a click without the hit strokes having to be disabled to protect it. */
.mpt-links{
  position:absolute;left:0;top:0;
  overflow:visible;pointer-events:none;z-index:2;
}
.mpt-link{
  stroke:color-mix(in srgb,var(--muted) 60%,transparent);
  stroke-width:1.5;stroke-linejoin:round;
  transition:opacity .12s ease,stroke .12s ease;
}
.mpt-link-head{fill:color-mix(in srgb,var(--muted) 60%,transparent);transition:opacity .12s ease;}
/* A violated edge is one the dates contradict. Red is the platform's --red, and the dash
   carries the meaning without relying on colour alone. */
.mpt-link.is-violated{stroke:var(--amber);stroke-dasharray:5 4;stroke-width:1.5;}
.mpt-link-head.is-violated{fill:var(--amber);}
/* A violated order that has already happened keeps its shape and loses its alarm — the
   same treatment a past overloaded day gets in the capacity lane. You cannot reorder
   last month, so warning about it forever turns the layer into wallpaper. */
.mpt-link.is-violated.is-past{stroke:color-mix(in srgb,var(--muted) 60%,transparent);}
.mpt-link-head.is-violated.is-past{fill:color-mix(in srgb,var(--muted) 60%,transparent);}
.mpt-link-stub.is-violated.is-past{stroke:color-mix(in srgb,var(--muted) 60%,transparent);}

/* Keyboard focus must be unmistakable on a dense canvas. */
.mpt-bar:focus-visible,.mpt-milestone:focus-visible{
  outline:2px solid var(--highlight);outline-offset:2px;
}
/* The project header row is a keyboard stop too; ring it on the inside so a full-width
   row does not draw its outline under the rows above and below. */
.mpt-row-group:focus-visible{outline:2px solid var(--highlight);outline-offset:-2px;}
/* Keyboard selection we render ourselves. :focus-visible is a browser heuristic and is
   deliberately suppressed after a pointer interaction — so a row focused programmatically
   (returning from a modal closed with the mouse) would show no ring at all and the
   selection would look lost. This class is set by the keyboard model, so it always shows. */
.mpt-bar.is-kb-focus,.mpt-milestone.is-kb-focus{outline:2px solid var(--highlight);outline-offset:2px;}
.mpt-row-group.is-kb-focus{outline:2px solid var(--highlight);outline-offset:-2px;}
/* Announcements for keyboard moves: read out, never seen. */
.mpt-live{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
}

/* A backward edge cannot be drawn as a forward arrow without crossing the chart, so at
   rest only its endpoint stubs show. The full run appears while either end is hovered. */
.mpt-link.is-backward,
.mpt-link-head.is-backward{opacity:0;}
.mpt-links.has-focus .mpt-link.is-backward.is-active,
.mpt-links.has-focus .mpt-link-head.is-backward.is-active{opacity:1;}
.mpt-link-stub{
  stroke:var(--amber);stroke-width:2.5;stroke-linecap:round;
  transition:opacity .12s ease;
}
.mpt-link-stub:not(.is-violated){stroke:color-mix(in srgb,var(--muted) 70%,transparent);}

/* Hover affinity: the relationships of the bar you are pointing at come forward, the
   rest recede. Without it a chart with more than a few links is an undifferentiated
   thicket. */
.mpt-links.has-focus .mpt-link:not(.is-active):not(.is-backward),
.mpt-links.has-focus .mpt-link-head:not(.is-active):not(.is-backward),
.mpt-links.has-focus .mpt-link-stub:not(.is-active){opacity:.18;}
.mpt-links.has-focus .mpt-link.is-active{stroke-width:2.25;}

/* The connector layer paints ABOVE the rows, so an 11px-wide hit stroke crossing a bar
   would swallow clicks meant for that bar — pressing a task could delete a dependency
   instead of opening it. Hit paths are therefore inert until their own edge is the one
   being looked at, which is the same affinity rule the lines already follow: hover either
   endpoint, then the connector is clickable. */
/* Cut control: the actual target for removing a dependency. Appears with its edge, sized
   like a real button rather than a hairline. */
.mpt-link-cut{opacity:0;pointer-events:none;transition:opacity .12s ease;}
.mpt-links.has-focus .mpt-link-cut.is-active{opacity:1;pointer-events:auto;cursor:pointer;}
.mpt-link-cut-hit{fill:transparent;}
.mpt-link-cut-dot{
  fill:var(--panel-solid);
  stroke:color-mix(in srgb,var(--muted) 55%,transparent);stroke-width:1;
}
.mpt-link-cut-x{stroke:var(--muted);stroke-width:1.6;stroke-linecap:round;fill:none;}
.mpt-link-cut:hover .mpt-link-cut-dot{stroke:var(--red);fill:color-mix(in srgb,var(--red) 12%,var(--panel-solid));}
.mpt-link-cut:hover .mpt-link-cut-x{stroke:var(--red);}

/* Live everywhere. The layer sits under the rows now, so a bar always wins a click that
   lands on it and the hit stroke no longer has to be switched off to protect one. That in
   turn means hovering ANY part of a connection activates it — before, you had to touch an
   endpoint first, because the line could not be hovered until it was already active. */
.mpt-link-hit{
  stroke:transparent;stroke-width:14;
  pointer-events:stroke;cursor:pointer;
}
.mpt-link-hit:hover{stroke:color-mix(in srgb,var(--amber) 22%,transparent);}


/* Link mode. Every bar is a candidate, so the whole canvas says so rather than a
   single hover dot; the chosen predecessor stays marked while you pick the successor. */
.mpt.is-linking .mpt-bar,
.mpt.is-linking .mpt-milestone{cursor:crosshair;}
.mpt.is-linking .mpt-bar{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--highlight) 45%,transparent);}
.mpt-bar.is-link-source,
.mpt-milestone.is-link-source{
  box-shadow:inset 0 0 0 2px var(--highlight),0 0 0 3px color-mix(in srgb,var(--highlight) 30%,transparent);
}
/* Mode banner: states the next action so the mode is never guessed at. */
/* The cascade question. Same strip vocabulary as link mode, because it is the same kind
   of thing: a statement of what happens next with the chart still visible behind it —
   the decision is about which bars move, so hiding the bars to ask it would be odd. */
.mpt-cascadebar{
  display:flex;align-items:center;gap:10px;
  margin:0 0 8px;padding:8px 12px;
  border:1px solid color-mix(in srgb,var(--amber) 38%,transparent);
  border-radius:10px;
  background:color-mix(in srgb,var(--amber) 12%,transparent);
  font-size:var(--mp-fs-sm,12px);
}
.mpt-cascadebar .mpt-linkbar-text{flex:1 1 auto;min-width:0;}

.mpt-linkbar{
  display:flex;align-items:center;gap:10px;
  padding:7px 12px;
  background:color-mix(in srgb,var(--highlight) 12%,transparent);
  border-bottom:1px solid color-mix(in srgb,var(--highlight) 30%,transparent);
}
.mpt-linkbar-text{
  flex:1 1 auto;
  font-size:var(--mp-fs-sm,12px);font-weight:var(--mp-fw-semibold,600);color:var(--text);
}

/* ---------- signal layer ----------
   Three distinct states, three distinct channels, so they can co-occur on one bar without
   becoming a colour puzzle:
     critical  — structural: no room before it pushes something. Weight, not colour.
     overdue   — temporal: finished in the past and still open. Amber ring.
     violated  — relational: an edge the dates contradict. Red dashed connector.
   Red stays reserved for "the plan contradicts itself", which is why overdue is amber. */
/* Critical widens the rail VISUALLY via an inset shadow, never by changing
   border-left-width. The knobs and the link handle are absolutely positioned, and an
   absolutely positioned child anchors to the PADDING box — so a fatter border slides them
   inward. On a 25px bar that was enough for the start knob to cover the bar's own centre,
   making a short critical task impossible to move and only resizable. State must never
   move a hit target. */
.mpt-bar.is-critical{
  box-shadow:inset 3px 0 0 0 var(--mpt-acc),
             inset 0 0 0 var(--mp-block-hair,.5px) color-mix(in srgb,var(--mpt-acc) 80%,transparent),
             0 0 0 1px color-mix(in srgb,var(--mpt-acc) 45%,transparent);
}
.mpt-bar.is-overdue{
  box-shadow:inset 0 0 0 var(--mp-block-hair,.5px) color-mix(in srgb,var(--mpt-acc) 55%,transparent),
             0 0 0 2px color-mix(in srgb,var(--amber) 65%,transparent);
}
.mpt-bar.is-critical.is-overdue{
  box-shadow:inset 3px 0 0 0 var(--mpt-acc),
             inset 0 0 0 var(--mp-block-hair,.5px) color-mix(in srgb,var(--mpt-acc) 80%,transparent),
             0 0 0 2px color-mix(in srgb,var(--amber) 75%,transparent);
}
.mpt-milestone.is-overdue{box-shadow:0 0 0 2px color-mix(in srgb,var(--amber) 70%,transparent);}

/* A link between two critical tasks is a segment of the path, and reads as one. */
.mpt-link.is-critical{stroke-width:2.5;stroke:color-mix(in srgb,var(--text) 55%,transparent);}
.mpt-link-head.is-critical{fill:color-mix(in srgb,var(--text) 55%,transparent);}

/* ---------- window edges ----------
   A period window shows one period, and work crosses its edges. The bar is clipped to the
   lane (clampToLane in timeline.js) — but a clipped bar flush against the boundary looks
   exactly like a bar that STARTS there, which is the same lie the unclipped version told,
   only tidier. So the cut end is squared off and given a torn edge: the rounded corner is
   what says "this is where it begins", and its absence says the opposite.

   The notch is a repeating-linear-gradient rather than a glyph so it costs no element, scales
   with the bar's height, and cannot be clipped away by .mpt-bar-clip. */
.mpt-bar.is-cut-start,.mpt-plan.is-cut-start,.mpt-rollup.is-cut-start{
  border-top-left-radius:0;border-bottom-left-radius:0;
  border-left:0;
}
.mpt-bar.is-cut-end,.mpt-plan.is-cut-end,.mpt-rollup.is-cut-end{
  border-top-right-radius:0;border-bottom-right-radius:0;
}
.mpt-bar.is-cut-start::before,.mpt-bar.is-cut-end::after{
  content:"";position:absolute;top:0;bottom:0;width:5px;pointer-events:none;z-index:4;
  background:repeating-linear-gradient(
    -45deg,
    var(--mpt-acc) 0 2px,
    transparent 2px 4px);
  opacity:.85;
}
.mpt-bar.is-cut-start::before{left:0;}
.mpt-bar.is-cut-end::after{right:0;}

/* ---------- slack ----------
   How much room a task has, printed on the bar instead of hidden in a title attribute. This
   is the number that makes the critical path mean something: without it "this bar is drawn
   heavier" is a style, and with it the canvas states how many days there are to lose.
   Sits at the trailing edge, outside .mpt-bar-clip so the rounded box does not eat it. */
.mpt-slack{
  position:absolute;right:5px;top:50%;transform:translateY(-50%);
  font-size:var(--mp-fs-2xs,10px);font-weight:var(--mp-fw-semibold,600);
  font-variant-numeric:tabular-nums;line-height:1;
  padding:2px 4px;border-radius:var(--radius,5px);
  color:color-mix(in srgb,var(--text) 62%,transparent);
  background:color-mix(in srgb,var(--panel-solid) 78%,transparent);
  pointer-events:none;z-index:4;white-space:nowrap;
}
/* No room left, and past due, get the platform's existing two voices — tight is structural,
   late is the plan contradicting itself. Same reservation of red as the signal layer above. */
.mpt-slack.is-tight{color:color-mix(in srgb,var(--text) 88%,transparent);}
.mpt-slack.is-late{color:var(--red);background:color-mix(in srgb,var(--red) 14%,var(--panel-solid));}

/* Critical-path focus: everything with room to spare recedes. Not hidden — a plan you
   cannot see the rest of is not a plan. */
.mpt.is-critical-only .mpt-row-task:not(:has(.mpt-bar.is-critical)):not(:has(.mpt-milestone.is-critical)){
  opacity:.32;
}
.mpt.is-critical-only .mpt-row-task:hover{opacity:1;}

/* Owner face in the label column: visible at every zoom, unlike anything inside a 12px
   bar. Colour comes from AvatarService through --mp-av, never a local palette. */
/* Size only. Colour, ink and ring come from .mpdg-avatar so the timeline's faces are the
   same object as the grid's — including the scheme-dependent glyph colour, which a private
   rule here got wrong (white initials in the dark scheme, where the platform uses dark). */
.mpt-avatar{
  flex:none;
  width:20px;height:20px;
  font-size:var(--mp-fs-3xs,9px);
  text-transform:uppercase;
  user-select:none;
}

@media (prefers-reduced-motion:reduce){
  .mpt-knob::before,.mpt-hint,.mpt-twist .mp-icon{transition:none;}
}


@media (max-width:900px){
  .mpt{--mpt-label-w:150px;}
  .mpt-label{padding:6px 8px;}
  .mpt-label-child{padding-left:18px;}
  .mpt-scroll{height:var(--mpt-scroll-h,60vh);}
}

/* On a coarse pointer the bars and tray chips are not drag handles (timeline.js gates the
   pointerdown on pointer:coarse), so the browser should own every touch: a swipe over a bar
   scrolls the rows instead of being swallowed by a gesture that no longer exists. Without
   this the canvas eats vertical pans and the plan feels stuck. Horizontal panning of time
   stays with the scroll container, which is where it belongs. */
@media (pointer: coarse){
  .mpt-bar,.mpt-chip{touch-action:auto;}
}

/* ============================================================================
   PRINT

   Two of the three problems are solved in JS before this runs: every row is rendered
   (virtualisation would otherwise print spacers) and the scale is fitted to the page.
   What is left here is turning a scrolling, layered canvas into a static document.

   The label column needs no special handling: it is part of each row, not a separate
   pane, so it repeats on every page for free. The DATE HEADER does not - it is a single
   element and prints on the first page only. Repeating it would mean rebuilding the
   canvas as a real table with a thead, which is the one thing here worth its own decision.
   ============================================================================ */
@page{size:A4 landscape;margin:10mm;}
@media print{
  /* The bars ARE the information. A printer that drops backgrounds prints an empty grid. */
  .mpt,.mpt *{-webkit-print-color-adjust:exact;print-color-adjust:exact;}

  /* A card border and a 14px radius around a document that runs over four pages reads as
     a box that failed to close.

     display:block undoes the fill column. Paper has no viewport to fill and no scroll port to
     leave room for, so a flex item sized to "whatever is left" has nothing to be left of — the
     document has to be as tall as its content and paginate. */
  .mpt{display:block;border:0;border-radius:0;background:none;overflow:visible;}

  /* Nothing below can be operated on paper. */
  .mpt-toolbar,.mpt-hint,.mpt-slot,.mpt-knob,.mpt-unplan,.mpt-twist,
  .mpt-link-cut,.mpt-linkbar,.mpt-cascadebar,.mpt-plan-ghost,.mpt-unplanned{
    display:none !important;
  }

  /* One document instead of a scroll port: no clipping, no fixed height, and no sticky
     layers - sticky resolves against a scroll container that no longer exists, which is
     what makes a naive print of this surface come out as one clipped screenful. */
  .mpt-scroll{overflow:visible !important;height:auto !important;}
  .mpt-grid,.mpt-body{overflow:visible !important;}
  .mpt-head,.mpt-head-label,.mpt-label,.mpt-cap-lane,.mpt-cap-label{position:static !important;}

  /* A row split across a page break is a bar cut in half, and a project header stranded at
     the foot of a page is a heading for rows that are not there. */
  .mpt-row{break-inside:avoid;page-break-inside:avoid;}
  .mpt-row-group{break-after:avoid;page-break-after:avoid;}
  .mpt-cap-row{break-inside:avoid;page-break-inside:avoid;}
}
