@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Source+Serif+4:opsz,wght@8..60,600;8..60,700&display=swap');

:root{
  --blue:#0000EF; --blue-dark:#0000C4; --blue-soft:#8E93FF; --blue-wash:#EEF0FF;
  --ink:#111827; --grey:#6B7280; --faint:#9CA3AF; --rule:#E5E7EB; --paper:#FBFBFD; --card:#FFFFFF;
  --green:#0E9F6E; --green-wash:#E7F7F0; --amber:#B45309; --amber-wash:#FEF3C7; --red:#DC2626; --red-wash:#FEE2E2;
  --radius:16px; --shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
}
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{font-family:'Montserrat',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--paper);color:var(--ink);line-height:1.45;min-height:100vh;-webkit-font-smoothing:antialiased}
button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}
input{font:inherit;color:inherit}
h1,h2,h3{font-family:'Source Serif 4',Georgia,serif;line-height:1.15;letter-spacing:-.01em}
.hidden{display:none!important}

/* top bar */
.bar{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;background:var(--blue);color:#fff;position:sticky;top:0;z-index:20}
.brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.14em;font-size:12px;text-transform:uppercase}
.brand i{display:grid;place-items:center;width:28px;height:28px;border-radius:8px;background:#fff;color:var(--blue);font-style:normal;font-weight:900;letter-spacing:0;font-size:13px}
.brand small{display:block;font-weight:600;letter-spacing:.1em;opacity:.75;font-size:10px}
.bar .who{font-size:12px;font-weight:600;opacity:.95;text-align:right}
.bar .who button{display:block;margin-left:auto;font-size:11px;text-decoration:underline;opacity:.8;color:#fff}

.wrap{max-width:520px;margin:0 auto;padding:20px 16px 120px}
.wrap.wide{max-width:1280px}

/* steps */
.progress{display:flex;gap:6px;margin:4px 0 22px}
.progress span{flex:1;height:4px;border-radius:4px;background:var(--rule)}
.progress span.on{background:var(--blue)}
.kicker{font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--blue);margin-bottom:8px}
h1{font-size:30px;margin-bottom:6px}
h2{font-size:24px;margin-bottom:6px}
.sub{color:var(--grey);font-size:14px;margin-bottom:18px}

/* tiles */
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.tile{background:var(--card);border:1.5px solid var(--rule);border-radius:14px;padding:16px 12px;font-weight:700;font-size:15px;text-align:left;transition:.12s;min-height:56px;display:flex;flex-direction:column;justify-content:center}
.tile small{font-weight:500;color:var(--grey);font-size:11px;margin-top:2px}
.tile:active{transform:scale(.98)}
.tile:hover,.tile.sel{border-color:var(--blue);background:var(--blue-wash)}
.tile.sel{box-shadow:inset 0 0 0 1px var(--blue)}
.tile[disabled]{opacity:.45;cursor:not-allowed;background:var(--paper)}
.tile[disabled]:hover{border-color:var(--rule);background:var(--paper)}

.field{width:100%;padding:15px 16px;border:1.5px solid var(--rule);border-radius:14px;background:#fff;font-size:16px;font-weight:500;outline:none}
.field:focus{border-color:var(--blue);box-shadow:0 0 0 4px var(--blue-wash)}
.list{margin-top:10px;display:flex;flex-direction:column;gap:8px}
.row{display:block;width:100%;text-align:left;background:#fff;border:1.5px solid var(--rule);border-radius:14px;padding:13px 14px}
.row:hover{border-color:var(--blue)}
.row b{display:block;font-size:14px}
.row span{font-size:12px;color:var(--grey)}
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{padding:11px 16px;border-radius:999px;border:1.5px solid var(--rule);background:#fff;font-weight:700;font-size:14px}
.chip.sel{background:var(--blue);border-color:var(--blue);color:#fff}
.label{font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--grey);margin:22px 0 8px}

/* buttons */
.btn{display:block;width:100%;padding:17px;border-radius:14px;background:var(--blue);color:#fff;font-weight:800;font-size:16px;text-align:center;box-shadow:0 6px 18px rgba(0,0,239,.25)}
.btn:active{background:var(--blue-dark)}
.btn[disabled]{background:var(--rule);color:var(--faint);box-shadow:none;cursor:not-allowed}
.btn.ghost{background:#fff;color:var(--blue);border:1.5px solid var(--blue);box-shadow:none}
.btn.green{background:var(--green);box-shadow:0 6px 18px rgba(14,159,110,.25)}
.btn.small{display:inline-block;width:auto;padding:9px 14px;font-size:13px;border-radius:10px;box-shadow:none}
.link{color:var(--grey);font-size:13px;text-decoration:underline;font-weight:600}
.back{font-size:13px;font-weight:700;color:var(--grey);margin-bottom:14px}
.dock{position:fixed;left:0;right:0;bottom:0;padding:12px 16px calc(12px + env(safe-area-inset-bottom));background:linear-gradient(to top,var(--paper) 70%,transparent);z-index:10}
.dock>div{max-width:520px;margin:0 auto}

/* cards / status */
.card{background:#fff;border:1.5px solid var(--rule);border-radius:var(--radius);padding:18px;box-shadow:var(--shadow)}
.hero{border-radius:20px;padding:26px 22px;color:#fff;background:var(--blue);box-shadow:var(--shadow)}
.hero .big{font-family:'Source Serif 4',Georgia,serif;font-size:40px;font-weight:700;line-height:1.05;margin:6px 0}
.hero p{opacity:.9;font-size:14px}
.hero.ok{background:var(--green)} .hero.warn{background:var(--amber)} .hero.bad{background:var(--red)}
.meta{display:grid;grid-template-columns:auto 1fr;gap:8px 14px;font-size:14px;margin-top:16px}
.meta dt{color:var(--grey);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.08em;padding-top:2px}
.meta dd{font-weight:600}
.pulse{display:inline-block;width:10px;height:10px;border-radius:50%;background:#fff;margin-right:8px;animation:pulse 1.4s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(255,255,255,.7)}100%{box-shadow:0 0 0 14px rgba(255,255,255,0)}}
.banner{background:var(--blue-wash);border:1.5px solid var(--blue);border-radius:var(--radius);padding:16px;margin-bottom:16px}
.banner h3{font-size:18px;margin-bottom:4px}
.banner p{font-size:13px;color:var(--grey);margin-bottom:12px}
.banner .two{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.est{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.est .tile{align-items:center;text-align:center;padding:12px 6px}
.est .tile b{font-size:20px;font-family:'Source Serif 4',serif}
.minimap{height:170px;border-radius:14px;overflow:hidden;border:1.5px solid var(--rule);margin-top:14px}
.stepper{display:flex;align-items:center;gap:14px}
.stepper button{width:44px;height:44px;border-radius:12px;border:1.5px solid var(--rule);background:#fff;font-size:22px;font-weight:700}
.stepper b{font-size:20px;min-width:90px;text-align:center}
.err{background:var(--red-wash);color:var(--red);padding:12px 14px;border-radius:12px;font-size:13px;font-weight:600;margin-bottom:14px}
.note{font-size:12px;color:var(--grey);margin-top:8px}
.skeleton{height:68px;border-radius:14px;background:linear-gradient(90deg,#eee,#f7f7f7,#eee);background-size:200% 100%;animation:sk 1.2s infinite}
@keyframes sk{to{background-position:-200% 0}}

/* dashboard */
.dash{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:16px;align-items:start}
@media(max-width:900px){.dash{grid-template-columns:1fr}}
#map{height:calc(100vh - 210px);min-height:380px;border-radius:var(--radius);border:1.5px solid var(--rule);overflow:hidden;position:sticky;top:76px}
@media(max-width:900px){#map{position:static;height:340px}}
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}
@media(max-width:600px){.stats{grid-template-columns:1fr 1fr}}
.stat{background:#fff;border:1.5px solid var(--rule);border-radius:14px;padding:12px 14px}
.stat b{display:block;font-family:'Source Serif 4',serif;font-size:30px;line-height:1}
.stat span{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--grey)}
.stat.bad{border-color:var(--red);background:var(--red-wash)} .stat.bad b{color:var(--red)}
.stat.warn b{color:var(--amber)}
.so{background:#fff;border:1.5px solid var(--rule);border-left:6px solid var(--blue);border-radius:14px;padding:14px;margin-bottom:10px}
.so.pending{border-left-color:var(--faint)} .so.soon{border-left-color:#F59E0B} .so.overdue{border-left-color:var(--red);background:var(--red-wash)} .so.active{border-left-color:var(--green)}
.so h3{font-family:'Montserrat',sans-serif;font-size:15px;font-weight:800;margin-bottom:2px}
.so .line{font-size:13px;color:var(--grey)}
.so .top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
.badge{font-size:10px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;padding:4px 8px;border-radius:999px;background:var(--rule);white-space:nowrap}
.badge.active{background:var(--green-wash);color:var(--green)} .badge.soon{background:var(--amber-wash);color:var(--amber)} .badge.overdue{background:var(--red);color:#fff} .badge.pending{background:var(--rule);color:var(--grey)}
.so .actions{display:flex;gap:8px;margin-top:10px}
.tabs{display:flex;gap:6px;margin:0 0 12px}
.tab{padding:8px 14px;border-radius:999px;font-weight:700;font-size:13px;border:1.5px solid var(--rule);background:#fff}
.tab.sel{background:var(--ink);border-color:var(--ink);color:#fff}
.gate{max-width:360px;margin:12vh auto;text-align:center}
.empty{text-align:center;color:var(--grey);padding:40px 10px;font-size:14px}
.roster-row{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--rule);font-size:14px;font-weight:600}
.pin{width:14px;height:14px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.4)}
.demo-flag{background:#FEF3C7;color:#92400E;font-size:11px;font-weight:700;text-align:center;padding:6px}

/* ── Map markers on the RA board ──────────────────────────────────────────
   The house carries the headcount, because "who is in the building" is the
   question the board exists to answer. Vehicles are placed by elapsed time,
   not by GPS — their tooltips say "simulated" so the board never overstates
   what it knows. */
.m-home {
  width: 54px; height: 54px; border-radius: 50%;
  background: var(--blue); color: #fff; border: 3px solid #fff;
  box-shadow: 0 2px 10px rgba(0,0,0,.28);
  display: grid; place-content: center; text-align: center; line-height: 1;
}
.m-home b { display: block; font-size: 19px; font-weight: 800; }
.m-home span { display: block; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; opacity: .85; margin-top: 2px; }
/* A dot in motion. The halo pulses so a moving student reads differently from a
   parked destination pin at a glance, and the marker itself eases between the
   positions the tick hands it rather than snapping. */
.m-move {
  width: 34px; height: 34px; border-radius: 50%;
  background: #fff; border: 2px solid var(--blue);
  box-shadow: 0 2px 8px rgba(0,0,0,.25);
  display: grid; place-content: center; font-size: 17px;
  position: relative;
}
.m-move { border-color: var(--c, var(--blue)); }
.m-move::after {
  content: ''; position: absolute; inset: -6px; border-radius: 50%;
  border: 2px solid var(--c, var(--blue)); opacity: .6;
  animation: fs-pulse 1.8s ease-out infinite;
}
@keyframes fs-pulse {
  0%   { transform: scale(.72); opacity: .65; }
  70%  { transform: scale(1.35); opacity: 0; }
  100% { transform: scale(1.35); opacity: 0; }
}
/* Leaflet positions markers with transform, so easing it smooths the 500ms steps
   into continuous travel.

   But Leaflet ALSO drives its zoom animation with transforms on the same elements, and a
   transition on top of that makes every dot slide across the map and land somewhere wrong
   for half a second on each zoom. `.zooming` is set for the duration of the animation so
   the dots move with the map instead of chasing it. */
.leaflet-marker-icon:has(.m-move) { transition: transform .5s linear; }
.leaflet-container.zooming .leaflet-marker-icon,
.leaflet-container.zooming .leaflet-marker-icon:has(.m-move) { transition: none !important; }
.leaflet-container.zooming .m-move::after { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .m-move::after { animation: none; }
  .leaflet-marker-icon:has(.m-move) { transition: none; }
}

/* The routing sticker: a label pinned beside a moving dot, in that trip's colour, naming
   where it is headed. Sized in pixels and anchored to the marker, so zooming moves it with
   the map without rescaling or detaching. */
.leaflet-tooltip.m-sticker {
  background: var(--c, #374151); color: #fff; border: 0; border-radius: 7px;
  padding: 3px 8px; box-shadow: 0 2px 8px rgba(0,0,0,.3);
  font: 800 10.5px/1.25 'Inter', system-ui, sans-serif; letter-spacing: .02em;
  text-transform: uppercase; white-space: nowrap; pointer-events: none;
}
.leaflet-tooltip.m-sticker.leaflet-tooltip-right::before { border-right-color: var(--c, #374151); }
.leaflet-tooltip.m-sticker.leaflet-tooltip-left::before { border-left-color: var(--c, #374151); }
.leaflet-tooltip.m-sticker small {
  display: block; font-size: 9.5px; font-weight: 600; opacity: .85;
  letter-spacing: 0; text-transform: none; margin-top: 1px;
}
/* Hidden when it would land on another sticker; zoom in and it comes back. */
.leaflet-tooltip.m-sticker.hid { display: none; }
/* Leaflet fades a tooltip in when it opens, and these reopen whenever their marker's icon
   is swapped -- the fade read as a blink. They are pinned to a moving dot and do not need
   to announce themselves. */
.leaflet-tooltip.m-sticker { transition: none !important; }
/* Leaflet strips and re-adds `leaflet-tooltip-left/right` on every reposition, and those
   classes carry a 6px margin and the little arrow. Twice a second that is a label that
   visibly jumps and blinks. Neutralise both so the direction class has no visual effect
   and the churn becomes invisible. The sticker is already pinned to its dot; it does not
   need an arrow to say which one it belongs to. */
.leaflet-tooltip.m-sticker { margin: 0 !important; }
.leaflet-tooltip.m-sticker::before { display: none !important; }

/* The school. A second fixed place, deliberately quieter than the house: nobody
   signs out to go there, so it orients the map rather than competing with it. */
.m-school {
  min-width: 44px; height: 26px; padding: 0 9px; border-radius: 7px;
  background: #fff; color: var(--ink, #16161a); border: 2px solid var(--ink, #16161a);
  box-shadow: 0 2px 8px rgba(0,0,0,.2);
  display: grid; place-content: center;
  font: 800 10px/1 'Inter', system-ui, sans-serif; letter-spacing: .1em; text-transform: uppercase;
}

/* ── Tracking ─────────────────────────────────────────────────────────────
   The board used to show only the trips a student had signed out for, so most
   of an evening was an empty map. Now every student has a position, derived
   from the published schedule and the clock. Twenty separate dots at one
   address is a pile, so a place carries a count and expands on click. */
.m-grp {
  min-width: 58px; height: 30px; padding: 0 9px; border-radius: 9px;
  background: #fff; color: var(--ink, #16161a); border: 2px solid var(--ink, #16161a);
  box-shadow: 0 2px 8px rgba(0,0,0,.2);
  display: flex; align-items: center; justify-content: center; gap: 5px;
  font: 800 10px/1 'Inter', system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase;
  white-space: nowrap;
}
.m-grp b { font-size: 15px; letter-spacing: 0; }
.m-grp span { opacity: .72; }
.m-grp.school { border-color: var(--blue); color: var(--blue); }
/* A place that someone is travelling to wears the same colour as the dot on its way. */
.m-grp[style*="--c"] { border-color: var(--c); color: var(--c); }
/* An empty place still gets drawn -- an empty school at 9pm is information -- but it
   recedes, so a count of nought never reads as a crowd. */
.m-grp b:empty, .m-grp:has(b:only-child) { letter-spacing: 0; }
.m-grp:has(b) { opacity: 1; }

/* A quieter tile, for a number that is context rather than a headcount. */
.stat.sub { opacity: .72; }
.stat.sub b { font-size: 22px; }

/* The Itinerary tab */
.plan-head {
  font: 800 14px/1.3 'Inter', system-ui, sans-serif; padding: 2px 0 12px;
}
.plan-head small {
  display: block; margin-top: 4px; font: 500 12px/1.45 inherit; color: var(--grey, #6B7280);
}
.note-out {
  margin-top: 14px; border: 1px solid var(--rule, #E5E7EB); border-radius: 12px;
  padding: 12px 14px; background: #fff;
}
.note-out > summary {
  cursor: pointer; font-weight: 700; font-size: 13.5px; list-style: none;
}
.note-out > summary::marker, .note-out > summary::-webkit-details-marker { display: none; }
.note-out > summary::before { content: '\25B8 '; color: var(--grey, #6B7280); }
.note-out[open] > summary::before { content: '\25BE '; }
/* A journey carrying more than one student. The whole house leaving for the same open
   gym is one group walking down one street, so it is one dot with a count on it. */
.m-move i {
  position: absolute; top: -5px; right: -7px;
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: 9px;
  background: var(--c, var(--ink, #16161a)); color: #fff; border: 2px solid #fff;
  font: 800 10px/13px 'Inter', system-ui, sans-serif; font-style: normal;
  display: grid; place-content: center;
}
/* An empty place still gets drawn, but it recedes: a nought should not read as a crowd. */
.m-grp.none { opacity: .62; border-style: dashed; }

/* ── Family view ──────────────────────────────────────────────────────────
   A schedule row: time, what, where. Three columns so a parent can scan down
   the times rather than reading sentences. */
.pl-row {
  display: grid; grid-template-columns: 62px 1fr auto; gap: 10px; align-items: start;
  padding: 7px 0; border-top: 1px solid var(--rule, #E5E7EB); font-size: 13.5px; line-height: 1.45;
}
.pl-row:first-of-type { border-top: 0; }
.pl-t { font-weight: 700; color: var(--grey, #6B7280); font-variant-numeric: tabular-nums; }
.pl-k { font-size: 11px; color: var(--grey, #6B7280); text-align: right; max-width: 150px; }
.pl-row.past { opacity: .45; }
.pl-row.live { background: #EFF6FF; border-radius: 8px; padding-left: 8px; margin-left: -8px; }
.pl-row.live .pl-t { color: var(--blue); }
@media (max-width: 560px) {
  .pl-row { grid-template-columns: 54px 1fr; }
  .pl-k { display: none; }
}

/* The board framed on the landing page: a view, not a control panel. The demo tools
   carry an inline display:flex, hence the !important. Tiles tighten so the map is
   visible without scrolling the frame. */
body.peek .demo-flag, body.peek .bar .who button { display: none; }
/* The seed button stays: without a sign-out on the board there are no pairs and no
   sign-out paths, and that is half of what this view is showing. */
body.peek #demoTools { margin-bottom: 8px; }
body.peek #demoTools .btn { padding: 6px 12px; font-size: 12px; }
body.peek .stats { gap: 8px; }
body.peek .stat { padding: 10px 12px; }
body.peek .stat b { font-size: 22px; }
body.peek .stat span { font-size: 10px; }
body.peek .wrap { padding: 10px 14px 14px; }
/* The board's map is sticky so it stays beside a long list while the page scrolls. Inside
   the landing page's frame there is nothing to scroll past, and the 76px offset just
   pushed the map below the fold. */
/* Leaflet forces position:relative inline on its container, so `position: static` here
   loses -- but the sticky rule's `top: 76px` then applies to a relative element and pushes
   the map down by that much. Zero the offset instead. */
body.peek #map { top: 0; height: 380px; min-height: 0; }
/* One row of tiles in the frame, so the map is above the fold. */
body.peek .stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* Hover detail. The default Leaflet tooltip is a cramped white box; these carry an
   address and a list of names, so they need room to breathe and a readable measure. */
.leaflet-tooltip.m-tip {
  background: #fff; color: var(--ink, #16161a);
  border: 1.5px solid var(--rule, #E5E7EB); border-radius: 10px;
  box-shadow: 0 6px 20px rgba(0,0,0,.18);
  padding: 9px 12px; max-width: 260px; white-space: normal;
  font: 500 12.5px/1.5 'Inter', system-ui, sans-serif;
}
.leaflet-tooltip.m-tip b { font-weight: 800; font-size: 13px; }
.leaflet-tooltip.m-tip i { font-style: normal; color: var(--grey, #6B7280); }
.leaflet-popup.m-pop .leaflet-popup-content-wrapper {
  border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.22);
}
.leaflet-popup.m-pop .leaflet-popup-content {
  margin: 10px 13px; font: 500 12.5px/1.55 'Inter', system-ui, sans-serif; max-width: 240px;
}
.leaflet-popup.m-pop b { font-weight: 800; }
/* The house pin carries a flat's name under its count, so it needs a touch more room. */
.m-home { width: 58px; height: 58px; }
.m-home span { font-size: 8.5px; letter-spacing: .04em; }
