/*
 * app.css - the OPEM remote dashboard. Phone first (360 to 430 px wide), two
 * columns from 900 px. It must also hold at 277 px: a 360 px phone with the
 * page zoomed to 130% for larger text (sizes are px, so zoom is the route to
 * larger text). Nothing may be clipped there: the state pill keeps its full
 * width, the KPIs stack, the big figures scale with the width.
 *
 * The look is the floor TV's (C:\opem-tracker\display.py), so a person who
 * knows the screen on the wall reads this one without learning anything:
 *
 *   ember  #E2571E  live and normal, product is being made
 *   amber           attention: a lane drifting, a stop, the data is not live
 *   fault  #FF1E3C  a machine fault, used for nothing else, and always with
 *                   hazard stripes too, because ember and this red are about
 *                   1.07:1 apart under deuteranopia (the TV's palette notes)
 *
 * Degraded states remove colour rather than adding it: when the data is not
 * live (OFFLINE, or the cloud cannot be reached) the ember drains out of the
 * page (body.drained) and the absence reads as "not live" faster than a badge.
 *
 * System fonts only (no web font: nothing is loaded from anywhere but this
 * site), tabular figures everywhere so a number that changes does not shift
 * its neighbours. Nothing here is set from markup: the CSP allows styles from
 * this file only.
 */

/* ---------------------------------------------------------------- tokens */
:root{
  --ink:#131110; --panel:#1A1614; --panel2:#221C18; --rule:#2C2521;
  --fg:#F2ECE4; --fg2:#A79C90; --dim:#6B6157; --faint:#443C35;
  --ember:#E2571E; --ember-dim:#8A3512; --fault:#FF1E3C; --fault-deep:#8E0C1F;
  /* The TV's attention tier: the DROPOUT flag, the TEST MODE chip. */
  --amber:#F0A03C; --amber-hi:#FFC26B; --amber-bg:#5A3A10; --amber-fg:#FFD9A8;
  --sans:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,"SF Mono","Cascadia Mono",Consolas,"DejaVu Sans Mono",monospace;
}
*{box-sizing:border-box}
html{background:var(--ink);-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{margin:0;min-height:100vh;background:var(--ink);color:var(--fg);
  font-family:var(--sans);font-size:15px;line-height:1.4;
  font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;
  padding:env(safe-area-inset-top) env(safe-area-inset-right)
          env(safe-area-inset-bottom) env(safe-area-inset-left)}
[hidden]{display:none!important}
h1{font-size:22px;line-height:1.2;margin:0 0 10px;font-weight:800;letter-spacing:-.01em}
p{margin:0 0 12px}
b{font-weight:700}
button{font:inherit;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}
button:disabled{cursor:default;opacity:.5}
:focus-visible{outline:2px solid var(--amber-hi);outline-offset:2px}

/* ------------------------------------------------------------ the edge */
/* A bar along the top edge: ember while live, grey when not, hazard on a
   fault. Fixed, so it is there however far down the page is scrolled. */
#edge{position:fixed;top:0;left:0;right:0;height:4px;z-index:5;background:var(--faint)}
body.live #edge{background:var(--ember);box-shadow:0 0 10px rgba(226,87,30,.6)}
body.fault #edge{height:6px;background:repeating-linear-gradient(45deg,
  var(--fault) 0 8px,#2A0410 8px 16px);box-shadow:none}
body.drained #edge{background:var(--dim);box-shadow:none}

/* -------------------------------------------------------------- header */
.top{display:flex;align-items:center;gap:12px;max-width:1200px;margin:0 auto;
  padding:14px 14px 8px}
.brand{font-size:15px;font-weight:800;letter-spacing:.16em;white-space:nowrap;flex:1;
  min-width:0;overflow:hidden;text-overflow:ellipsis}
.brand b{color:var(--ember);font-weight:800}
body.drained .brand b{color:var(--fg2)}
.brand .sn{font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.06em;
  color:var(--dim);margin-left:10px}

.screen{max-width:1200px;margin:0 auto;padding:0 12px 28px}

/* ------------------------------------------------------------- buttons */
.primary{display:block;width:100%;border:0;border-radius:4px;padding:13px 16px;
  background:var(--ember);color:#180D06;font-size:16px;font-weight:800;letter-spacing:.01em}
.primary:active{filter:brightness(1.1)}
.ghost{background:transparent;border:1px solid var(--rule);border-radius:4px;
  padding:8px 12px;color:var(--fg2);font-size:14px;font-weight:600}
.ghost:active{background:var(--panel2)}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}
.row .primary{width:auto;flex:1}

/* --------------------------------------------------------------- cards */
.card{background:var(--panel);border-radius:6px;padding:14px 16px;margin:0 0 12px;
  position:relative;overflow:hidden}
.lab{font-size:12px;font-weight:800;letter-spacing:.16em;color:var(--fg2);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* "as of 14:05": on every section while the numbers are not live. */
.asof{margin-left:10px;color:var(--amber-hi);letter-spacing:.06em;font-weight:700}
.asof:empty{display:none}
.sub{font-family:var(--mono);font-size:12.5px;line-height:1.45;color:var(--fg2);margin-top:4px}
.sub2{font-family:var(--mono);font-size:11px;line-height:1.35;color:var(--fg2);margin-top:3px;
  min-height:15px}
.small{font-size:12.5px;color:var(--fg2);margin:10px 0 0}
.mid{font-size:28px;font-weight:900;letter-spacing:-.03em;line-height:1.08;white-space:nowrap}
/* A value that is not known. A word, small and grey: "no data" never reads as
   a number, and a dash at this size reads as a broken element (the TV's rule). */
.none{color:var(--dim)!important;font-weight:700!important;letter-spacing:0!important}
.mid.none{font-size:18px;line-height:31px}

/* --------------------------------------------------------- login, notes */
.login,.notice{max-width:460px;margin:7vh auto 0;padding:22px 20px}
.lead{color:var(--fg2)}
.login label{display:block;font-size:12px;font-weight:800;letter-spacing:.16em;color:var(--fg2);
  margin:6px 0 6px}
.login input{display:block;width:100%;margin:0 0 12px;padding:12px;border-radius:4px;
  border:1px solid var(--rule);background:var(--panel2);color:var(--fg);
  font-size:17px;font-family:var(--sans)}
.login input::placeholder{color:var(--dim)}
#code{font-family:var(--mono);font-size:22px;letter-spacing:.3em}
#codeform{margin-top:6px}
.msg{border-radius:4px;padding:10px 12px;font-size:14.5px;line-height:1.4;
  background:var(--amber-bg);color:var(--amber-fg)}
.msg.ok{background:var(--panel2);color:var(--fg)}
.big-ok{font-size:20px;font-weight:800;margin:6px 0 8px}

/* -------------------------------------------------------------- day nav */
/* Wraps rather than squeezing the date: on a narrow screen "Today" goes to
   the next line and the date keeps room to show the whole date. */
.daynav{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:2px 0 12px}
.navbtn{width:44px;height:44px;flex:0 0 44px;border:1px solid var(--rule);border-radius:4px;
  background:var(--panel);color:var(--fg);font-size:28px;line-height:1;padding:0 0 4px}
.navbtn:disabled{opacity:.3}
#date{flex:1 1 140px;min-width:0;height:44px;padding:0 10px;border:1px solid var(--rule);border-radius:4px;
  background:var(--panel);color:var(--fg);font:600 16px var(--sans);color-scheme:dark}
#totoday{height:44px}

/* A one-line note above the cards: the line down while a past day is open,
   or a browser that will not keep the login. */
.strip{margin:0 0 12px;padding:9px 12px;border-radius:4px;font-size:13.5px;line-height:1.4;
  background:var(--panel2);color:var(--amber-fg);border-left:4px solid var(--amber)}

/* --------------------------------------------------------------- banner */
/* Not live, said once, at the top, in words. Outside the drained area, so it
   keeps its colour while everything under it greys out. */
.banner{border-radius:6px;padding:14px 16px;margin:0 0 12px;
  background:var(--amber-bg);color:var(--amber-fg);border-left:6px solid var(--amber-hi)}
.btitle{font-size:22px;font-weight:900;letter-spacing:.02em;line-height:1.15;color:#FFE9CF}
.btext{font-size:14.5px;line-height:1.45;margin-top:6px}
.banner .ghost{margin-top:10px;color:var(--amber-fg);border-color:rgba(255,217,168,.4)}

/* ----------------------------------------------------------------- hero */
.statusrow{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin:0 0 14px;min-height:34px}
/* The pill never shrinks (a clipped "OFFLIN" says nothing); the words next to
   it wrap under it instead. */
.pill{flex:0 0 auto;font-size:15px;font-weight:900;letter-spacing:.12em;padding:7px 14px;border-radius:3px;
  white-space:nowrap;background:var(--panel2);color:var(--fg2);position:relative;overflow:hidden}
.pill.running{background:var(--ember);color:#180D06}
.pill.idle{background:var(--panel2);color:var(--fg)}
.pill.stalled,.pill.unknown{background:var(--amber-bg);color:var(--amber-fg)}
.pill.nolink{background:var(--panel2);color:var(--fg2);outline:1px dashed var(--dim);outline-offset:-1px}
.pill.offline,.pill.unreachable,.pill.loading{background:var(--panel2);color:var(--fg2)}
.pill.past{background:transparent;color:var(--fg);outline:1px solid var(--fg2);outline-offset:-1px}
/* Shape, not hue: hazard ends on the pill, as on the TV. */
.pill.fault{background:var(--fault);color:#210206;padding-left:30px;padding-right:30px;
  animation:pulse 1.1s steps(1,end) infinite}
.pill.fault::before,.pill.fault::after{content:"";position:absolute;top:0;bottom:0;width:16px;
  background:repeating-linear-gradient(45deg,#210206 0 5px,transparent 5px 10px)}
.pill.fault::before{left:0}
.pill.fault::after{right:0}
@keyframes pulse{0%,49%{opacity:1}50%,100%{opacity:.5}}
@media (prefers-reduced-motion:reduce){.pill.fault{animation:none}}
.fresh{flex:1 1 120px;font-family:var(--mono);font-size:12.5px;color:var(--fg2);line-height:1.3;min-width:0}

.big{font-size:clamp(40px,16.5vw,64px);font-weight:900;letter-spacing:-.045em;line-height:1;margin:6px 0 4px;
  white-space:nowrap}
.big.none{font-size:30px;line-height:1.6}
/* Three KPIs side by side. Each KPI's label, figure and note sit in shared
   grid rows (display:contents), so a label on two lines ("STOPS OVER 1 MIN")
   keeps the three figures level. */
.kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-template-rows:auto auto auto;
  grid-auto-flow:column;column-gap:10px;margin-top:14px;padding-top:12px;border-top:1px solid var(--rule)}
.kpi{display:contents}
.kpi .lab{font-size:11px;white-space:normal;line-height:1.25;align-self:end}
.kpis .mid{font-size:clamp(20px,7.4vw,28px)}
.kpis .mid.none{font-size:18px}

/* ---------------------------------------------------------------- chart */
.readout{display:flex;align-items:baseline;gap:8px;margin:6px 0 6px;min-height:26px;
  white-space:nowrap;overflow:hidden}
.readout b{font-size:22px;font-weight:900;letter-spacing:-.02em;color:var(--fg)}
.readout span{font-family:var(--mono);font-size:12px;color:var(--fg2);overflow:hidden;
  text-overflow:ellipsis}
.chartbox{position:relative;height:178px;touch-action:pan-y;user-select:none;
  -webkit-user-select:none}
.chart{display:block;width:100%;height:160px;overflow:visible}
.cline{fill:none;stroke:var(--ember);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.carea{fill:url(#fillgrad)}
/* A stop: a faint wash over its time and a solid strip along the time axis,
   so the strip reads at a glance and the wash does not bury the line. */
.cstops{fill:rgba(240,160,60,.07)}
.cstopbar{fill:var(--amber)}
body.drained .cstopbar{fill:var(--fg2)}
.grid0{stroke:var(--fg2);stroke-width:1;opacity:.5}
.gridl{stroke:var(--rule);stroke-width:1}
.cnow{stroke:var(--fg2);stroke-width:1;stroke-dasharray:2 3}
.ccross{stroke:var(--fg);stroke-width:1;opacity:0}
.cdot{fill:var(--ember);stroke:var(--panel);stroke-width:2;opacity:0}
.chartbox.touching .ccross,.chartbox.touching .cdot{opacity:1}
body.drained .cline{stroke:var(--fg2)}
body.drained .carea{fill:url(#fillgrey)}
body.drained .cdot{fill:var(--fg2)}
/* On the card's own colour, so the dotted now line or a stop passing behind
   does not cut through "240/min". */
.ylab{position:absolute;right:0;font-family:var(--mono);font-size:10px;color:var(--fg2);
  background:var(--panel);padding:0 3px;border-radius:2px;
  transform:translateY(-110%);pointer-events:none}
.xlabs{position:relative;height:18px}
.xlabs span{position:absolute;top:3px;transform:translateX(-50%);font-family:var(--mono);
  font-size:10.5px;color:var(--dim);white-space:nowrap}
.xlabs span.first{transform:none}
.xlabs span.last{transform:translateX(-100%)}
.legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:4px;font-family:var(--mono);
  font-size:11px;color:var(--fg2)}
.legend span{display:inline-flex;align-items:center;gap:6px}
.legend i{display:inline-block}
.k-line{width:14px;height:2px;background:var(--ember)}
body.drained .k-line{background:var(--fg2)}
.k-stop{width:12px;height:12px;background:linear-gradient(to top,var(--amber) 0 4px,rgba(240,160,60,.18) 4px)}
.k-fault{width:12px;height:12px;background:repeating-linear-gradient(45deg,
  rgba(255,30,60,.7) 0 3px,rgba(255,30,60,.12) 3px 6px)}
.k-gap{width:12px;height:12px;background:repeating-linear-gradient(45deg,
  rgba(167,156,144,.5) 0 2px,rgba(167,156,144,.06) 2px 5px)}

details.hours{margin-top:10px;border-top:1px solid var(--rule);padding-top:8px}
details summary{cursor:pointer;font-size:13px;font-weight:700;color:var(--fg2);padding:4px 0}

/* --------------------------------------------------------------- tables */
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin-top:6px}
.tbl th{font-size:11px;font-weight:800;letter-spacing:.08em;color:var(--fg2);text-align:left;
  padding:6px 3px;border-bottom:1px solid var(--rule);white-space:nowrap}
.tbl td{padding:8px 3px;border-bottom:1px solid var(--rule);vertical-align:top;white-space:nowrap}
.tbl .num{text-align:right}
.tbl td.num{font-weight:700}
.tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
.week tbody tr{cursor:pointer}
.week tbody tr:active{background:var(--panel2)}
.week tr.sel td{background:var(--panel2)}
.week tr.sel td:first-child{box-shadow:inset 3px 0 0 var(--ember)}
body.drained .week tr.sel td:first-child{box-shadow:inset 3px 0 0 var(--fg2)}
.week .d{font-weight:700;white-space:nowrap}
.week .d small{display:block;font-weight:500;font-size:11px;color:var(--fg2);white-space:normal}
.week .cause{display:block;font-weight:500;font-size:11px;color:var(--fg2);white-space:nowrap;
  max-width:92px;overflow:hidden;text-overflow:ellipsis;margin-left:auto}
.week tr.empty td{color:var(--dim)}

/* ---------------------------------------------------------------- stops */
.stoplist{list-style:none;margin:10px 0 0;padding:0}
.stoplist li{display:grid;grid-template-columns:50px 84px minmax(0,1fr);gap:8px;
  padding:8px 0;border-top:1px solid var(--rule);font-size:14px;align-items:baseline}
.stoplist .t{font-family:var(--mono);font-weight:700}
.stoplist .du{font-family:var(--mono);color:var(--fg)}
.stoplist .ca{color:var(--fg2);font-size:13px;overflow-wrap:anywhere}
.stoplist .ca i{font-style:normal;color:var(--fault)}
.stoplist .ca b{display:inline-block;margin-right:6px;padding:0 6px;border-radius:2px;
  font-size:11px;font-weight:900;letter-spacing:.12em;background:var(--amber-bg);color:var(--amber-fg)}
.stoplist li.fault .t{color:var(--fault)}
.stoplist li.fault .ca{color:var(--fg)}
#stopsmore{margin-top:10px}

/* -------------------------------------------------------------- rejects */
.rejtop{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-top:4px}
.rejmain{margin:8px 0 4px;font-size:14.5px}
.rejmain:empty{display:none}
.causes{display:grid;gap:7px;margin-top:10px}
.cause{display:grid;grid-template-columns:112px minmax(0,1fr) 54px;gap:10px;align-items:center;
  font-family:var(--mono);font-size:12.5px;color:var(--fg2);position:relative}
.cause b{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cause .bar{height:10px;background:var(--panel2);border-radius:2px;overflow:hidden}
.cause .bar i{display:block;height:100%;width:0;background:var(--ember-dim)}
.cause s{text-decoration:none;text-align:right;color:var(--fg)}
/* The worst cause by weight and a rule as well as by hue (the TV's rule). The
   rule is a border on the label: a ::before on a grid row became a grid item
   and pushed the row's columns sideways. */
.cause.worst b{font-weight:800;color:var(--fg);border-left:3px solid var(--ember);
  padding-left:6px;margin-left:-9px}
.cause.worst .bar i{background:var(--ember)}
body.drained .cause.worst .bar i{background:var(--fg2)}
body.drained .cause.worst b{border-left-color:var(--fg2)}

/* ---------------------------------------------------------------- lanes */
.lanes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px}
.lane{background:var(--panel2);border-radius:4px;padding:10px 12px 10px;position:relative}
.lane .n{font-size:11px;font-weight:800;letter-spacing:.16em;color:var(--fg2);line-height:17px}
.lane .v{font-size:22px;font-weight:900;letter-spacing:-.02em;line-height:1.15;margin-top:2px}
.lane .dv{font-family:var(--mono);font-size:12.5px;color:var(--fg2);margin-top:1px}
/* Deviation on a fixed scale, 10% fills half the track, so a 3% drift is
   a third of the way out and plain to see; a zero rule down the middle. */
.lane .track{position:relative;height:10px;margin-top:8px;background:var(--panel);border-radius:2px}
.lane .track::after{content:"";position:absolute;left:50%;top:-3px;bottom:-3px;width:2px;
  margin-left:-1px;background:var(--fg2);opacity:.6}
.lane .track i{position:absolute;top:0;bottom:0;left:50%;width:0;background:var(--ember)}
body.drained .lane .track i{background:var(--fg2)}
/* Both flags can be up at once (lane 2 on 09-30): they wrap under the name
   rather than run off the card. */
.lane .flag{display:inline-block;margin:1px 0 1px 6px;padding:0 5px;border-radius:2px;font-size:10px;
  line-height:15px;font-weight:900;letter-spacing:.12em;background:var(--amber-bg);color:var(--amber-fg);
  vertical-align:1px;white-space:nowrap}
.lane.warn{outline:2px solid var(--amber);outline-offset:-2px}
.lane.warn .v,.lane.warn .n{color:var(--amber-hi)}
.lane.warn .track i{background:var(--amber)}
.dropouts{margin-top:10px;font-size:13.5px;color:var(--fg2)}
.dropouts:empty{display:none}
.dropouts b{color:var(--amber-hi)}

/* ---------------------------------------------------------------- footer */
.foot{font-family:var(--mono);font-size:11.5px;line-height:1.6;color:var(--dim);padding:4px 4px 0}
.foot b{color:var(--fg2);font-weight:500;overflow-wrap:anywhere}

/* ----------------------------------------------------- not live, loading */
/* Colour drains out of everything under the banner. The ember elements are
   also set grey above, so the drain does not depend on the filter alone. */
body.drained .grid{filter:saturate(.15)}
/* While another day loads the old one stays, dimmed: no blank page, no jump. */
.grid{transition:opacity .2s}
.grid.loading,.grid.checking{opacity:.45}
body.booting .grid{opacity:.45}

/* ------------------------------------------------------------ narrow */
/* Under 380 px the 7-day table has no room for the cause line under each
   reject rate: the day view names it. */
@media (max-width:379px){
  .week .cause{display:none}
}
/* A 360 px phone zoomed for larger text (about 277 px): the KPIs stack, one
   per line, figure on the right, and the tables take their tightest padding. */
@media (max-width:340px){
  .kpis{grid-template-columns:minmax(0,1fr) auto;grid-template-rows:none;grid-auto-flow:row;
    row-gap:2px;align-items:baseline}
  .kpi .lab{align-self:baseline}
  .kpis .sub2{grid-column:1 / -1;margin:0 0 6px;min-height:0}
  .kpis .mid{font-size:24px;text-align:right}
  .tbl th{letter-spacing:.02em;padding:6px 2px}
  .tbl td{padding:8px 2px}
  .navbtn{width:40px;flex-basis:40px}
  /* The 7-day table, one day per block: the day on its own line, the four
     figures under it, each with its own small label (the header row goes). */
  .tbl.week,.tbl.week tbody{display:block}
  .tbl.week thead{display:none}
  .week tbody tr{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:4px;
    border-bottom:1px solid var(--rule);padding:6px 0}
  .week td{border-bottom:0;padding:2px}
  .week td.d{grid-column:1 / -1}
  .week .d small{display:inline;margin-left:8px}
  .week td.num::before{display:block;font-size:10px;font-weight:700;letter-spacing:.06em;color:var(--fg2)}
  .week td:nth-child(2)::before{content:"CUPS"}
  .week td:nth-child(3)::before{content:"UPTIME"}
  .week td:nth-child(4)::before{content:"STOPS"}
  .week td:nth-child(5)::before{content:"REJECTS"}
}

/* ------------------------------------------------------------- desktop */
@media (min-width:600px){
  .lanes{grid-template-columns:repeat(4,minmax(0,1fr))}
  .big{font-size:76px}
}
@media (min-width:900px){
  .screen{padding:0 20px 32px}
  .top{padding:16px 20px 10px}
  .grid{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:14px;
    grid-template-areas:"hero rej" "rate lanes" "stops week";align-items:start}
  .card{margin:0}
  .hero{grid-area:hero}
  .ratecard{grid-area:rate}
  .stopscard{grid-area:stops}
  .rejcard{grid-area:rej}
  .lanecard{grid-area:lanes}
  .weekcard{grid-area:week}
  .chartbox{height:218px}
  .chart{height:200px}
  .foot{margin-top:14px}
  .week .cause{max-width:200px}
}
