/* prry.nl — hand-written, no framework */

@font-face {
  font-family: 'CommitMono';
  src: url('../fonts/CommitMono-400.woff2?v=2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'CommitMono';
  src: url('../fonts/CommitMono-700.woff2?v=2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  color-scheme: light dark;
  --gray:   oklch(65% 0.01 286);
  --bg:     light-dark(#f7f7f8, #151516);
  --fg:     light-dark(oklch(21% 0.01 286), oklch(93% 0.005 286));
  --fg2:    light-dark(oklch(52% 0.01 286), oklch(72% 0.01 286));
  --fg3:    var(--gray);          /* decorative only — 3.03:1 in light */
  --line:   light-dark(oklch(88% 0.005 286), oklch(32% 0.008 286));
  --card:   light-dark(#ffffff, #1c1c1e);
  --accent: #ffff00;
  /* One measure for the whole site: header, network panel, cards and the
     status rows all end on the same right edge. */
  --content: 1020px;
  --pad: 40px;
  /* One spacing scale. Every margin in the header/panel region comes from
     here, so the vertical rhythm is a choice rather than an accident. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --font-mono: 'CommitMono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

html {
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

/* The single content shell. Every block inside caps at --content, so giving
   the shell that width plus its own padding makes all of them share one left
   and right edge without repeating a centring rule per block. */
body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 20px;
  padding: var(--pad);
  max-width: calc(var(--content) + var(--pad) * 2);
  margin-inline: auto;
  overflow-wrap: break-word;
  /* Digits are already fixed-advance in a monospace face; stating it means the
     figures still line up if the webfont fails and a fallback steps in. */
  font-variant-numeric: tabular-nums;
}

::selection { background: var(--accent); color: #000; }

a { color: inherit; text-decoration: none; }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.h2 { font-size: 12px; font-weight: 700; text-transform: uppercase; margin: 64px 0 16px; }
.h2::before { content: '## '; color: var(--fg3); }

.cursor { animation: flash 1s infinite; }
@keyframes flash { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

.dim { color: var(--fg3); }

/* ---- chrome ---- */
.hdr { max-width: var(--content); }
/* Baseline, not centre: these are two runs of text at the same size, so the
   baseline is what the eye actually reads as "aligned". */
.hdr-top { display: flex; justify-content: space-between; align-items: baseline;
           gap: var(--s2) var(--s5); flex-wrap: wrap; }
.wordmark { font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; }
.nav { display: flex; gap: 16px; flex-wrap: wrap; }
.nav a { color: var(--fg2); }
.nav a:hover, .nav a.on { color: var(--fg); }
.nav a.on { text-decoration: underline; text-underline-offset: 3px; }

/* This is an <h1> for document structure, not for emphasis — the UA default
   of 2em bold would break the 12px grid the whole page sits on. The gap above
   separates it from the chrome; the gap below is deliberately tighter, so the
   title reads as a caption on the summary rather than a floating block. */
.tagline { margin-top: var(--s6); text-transform: uppercase;
           font-size: 12px; font-weight: 400; line-height: 20px; }

/* Strict three columns, not auto-fit: the source order is
   Containers/Load/Memory then Domains/Storage/Uptime, and a resolved column
   count is what keeps that reading as two fixed rows instead of reflowing
   into whatever happens to fit. */
.summary { margin-top: var(--s4); display: grid;
           grid-template-columns: repeat(3, minmax(0, 1fr));
           column-gap: var(--s5); row-gap: var(--s1); }

/* One geometry for every metric: fixed label track, value takes the rest. */
.metric { display: grid; grid-template-columns: 90px minmax(0, 1fr);
          align-items: baseline; column-gap: var(--s3); }
.metric dt { color: var(--fg2); text-transform: uppercase; }
.metric dd { color: var(--fg); min-width: 0; }

/* Three levels of emphasis, by colour rather than size: the figure carries the
   line, its unit sits a step back, trailing context sits further back again. */
.u { color: var(--fg2); }
.summary[data-stale] dd, .summary[data-stale] .u { color: var(--fg2); }

/* ---- live network ---- */
.net { margin-top: var(--s6); border: 1px solid var(--line); padding: var(--s4);
       max-width: var(--content); }

/* Title left, provenance right, on one baseline. flex-wrap lets the metadata
   drop to its own line on a narrow screen instead of crushing the title. */
.net-hd { display: flex; justify-content: space-between; align-items: baseline;
          gap: var(--s2) var(--s4); flex-wrap: wrap; margin-bottom: var(--s4); }
.net-t { font-weight: 700; text-transform: uppercase; }
.net-t::before { content: '## '; color: var(--fg3); font-weight: 400; }

/* IN / OUT / CONNTRACK as three equal tracks. minmax(0,1fr) rather than 1fr so
   a long value is clipped by its own column instead of widening it and
   shunting the other two sideways. */
.net-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
             column-gap: var(--s6); }
.net-stat { min-width: 0; }
.net-stat dt, .net-xfer dt { color: var(--fg2); text-transform: uppercase; }
.net-stat dd { display: flex; gap: var(--s2); align-items: baseline;
               color: var(--fg); min-width: 0; }
.net .v { font-weight: 700; }

/* Lifetime totals are context, not a live reading — same label-over-value
   shape as the stats above, but it is one phrase and must not break mid-way. */
.net-xfer { margin-top: var(--s4); }
.net-xfer dd { color: var(--fg); white-space: nowrap;
               overflow: hidden; text-overflow: ellipsis; }

/* A dead sampler must not look like live zeroes. */
.net[data-offline] { border-style: dashed; }
.net[data-offline] .v { color: var(--fg3); font-weight: 400; }
.net[data-offline] .graph-track i { background: var(--line); }

/* ---- throughput graphs ----
   Fixed label track, chart takes every remaining pixel of the panel. Both rows
   use the same definition, so RX and TX start and end on the same X. */
.graph-list { margin-top: var(--s4); display: grid; row-gap: var(--s2); }
.graph-row { display: grid; grid-template-columns: 64px minmax(0, 1fr);
             column-gap: var(--s3); align-items: end; }
/* No uppercase transform: the label is written as "RX · 60s" and the unit
   belongs in lower case. --fg2 rather than --fg3 because this is a real label,
   and --fg3 is the decorative tier that misses AA against the light ground. */
.graph-label { color: var(--fg2); white-space: nowrap; }

/* One track per sample, equal 1fr widths, so the chart fills the panel exactly
   at any width and every bar keeps the same footing. Heights are eight fixed
   steps — the same quantisation the block-glyph sparkline had — as classes
   rather than inline styles, because the CSP sets style-src 'self' and would
   block a style attribute outright. */
.graph-track { display: grid; grid-template-columns: repeat(60, 1fr); gap: 2px;
               align-items: end; height: 28px; }
.graph-track i { display: block; min-width: 0; background: var(--fg2); }
.graph-track .l1 { height: 12.5%; }
.graph-track .l2 { height: 25%; }
.graph-track .l3 { height: 37.5%; }
.graph-track .l4 { height: 50%; }
.graph-track .l5 { height: 62.5%; }
.graph-track .l6 { height: 75%; }
.graph-track .l7 { height: 87.5%; }
.graph-track .l8 { height: 100%; }
/* Time the sampler has not covered yet: a placed cell with nothing drawn, so a
   short history stays right-aligned instead of sliding the newest sample. */
.graph-track .l0 { height: 0; }

/* Visually hidden, still read by screen readers — the sparklines are aria-hidden. */
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip-path: inset(50%); white-space: nowrap; }

.ftr { margin-top: 80px; padding-top: 20px; border-top: 1px solid var(--line);
       color: var(--fg2); text-align: center; }

/* ---- project cards ---- */
main { max-width: var(--content); }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}

.card {
  display: flex; flex-direction: column;
  background: var(--bg);
  padding: 16px;
  transition: background-color 120ms ease;
}
a.card:hover { background: var(--card); }

.card-hd { display: flex; align-items: center; gap: 6px; }
.card-name { font-weight: 700; text-transform: uppercase; }

.arw { flex: none; color: var(--fg3); transition: transform 120ms ease, color 120ms ease; }
a.card:hover .arw { color: var(--fg); transform: translate(1px, -1px); }

.card-meta { display: flex; justify-content: space-between; gap: 12px;
             color: var(--fg2); text-transform: uppercase; }
.card-desc { margin-top: 12px; color: var(--fg2); flex: 1; }
.card-tech { margin-top: 12px; color: var(--fg3); text-transform: uppercase;
             overflow-wrap: anywhere; }

/* ---- contact ---- */
.contact { max-width: 520px; }
.form { display: flex; flex-direction: column; gap: 8px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }

.fi {
  width: 100%; padding: 8px 10px;
  font-family: var(--font-mono); font-size: 12px; line-height: 20px;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); outline: none;
  transition: border-color 120ms ease;
}
.fi::placeholder { color: var(--fg3); text-transform: uppercase; }
.fi:focus { border-color: var(--fg); }
textarea.fi { resize: vertical; min-height: 100px; }

.btn {
  align-self: flex-start; padding: 8px 20px;
  font-family: var(--font-mono); font-size: 12px; font-weight: 700;
  text-transform: uppercase; color: var(--bg); background: var(--fg);
  border: 1px solid var(--fg); cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.btn:hover { background: var(--bg); color: var(--fg); }

.msg { padding: 8px 10px; border: 1px solid var(--line); margin-bottom: 8px; }
.msg-ok  { border-color: var(--fg); }
.msg-err { border-left: 3px solid var(--accent); }

/* ---- uses ---- */
.lede { margin-top: 24px; max-width: 62ch; color: var(--fg2); }
.spec { max-width: 80ch; border-top: 1px solid var(--line); }
.spec > div { display: grid; grid-template-columns: 120px 1fr; gap: 16px;
              padding: 12px 0; border-bottom: 1px solid var(--line); }
.spec dt { color: var(--fg2); text-transform: uppercase; }
.spec dd { color: var(--fg); }

/* ---- beta banner ---- */
.beta {
  display: inline-block; margin-bottom: 24px; padding: 4px 10px;
  border: 1px solid var(--fg); text-transform: uppercase;
}
.beta b { background: var(--accent); color: #000; padding: 0 4px; font-weight: 700; }

/* These breakpoints come from measured intrinsic widths, not round numbers.
   The widest summary value is DOMAINS at 194px; with the 90px label track and
   its gap, a column needs 296px, so three columns want 937px of content and
   two want 617px. The thresholds below sit above those with enough headroom
   for the visitor count to reach four digits. Below them the value wrapped
   mid-phrase, which is what put "today" on its own line. */
@media (max-width: 1040px) {
  .summary { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--s2); }
}

/* The network stats stay at three columns well past this — their widest value
   needs 151px — and splitting them would cost the IN/OUT comparison that is
   the point of the row. Only the gutter tightens. */
@media (max-width: 860px) {
  .net-stats { column-gap: var(--s5); }
}

@media (max-width: 760px) {
  .summary { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 640px) {
  :root { --pad: 20px; }
  .h2 { margin-top: 48px; }
  .form-row { grid-template-columns: 1fr; }
  .spec > div { grid-template-columns: 1fr; gap: 4px; }

  /* Long single-line banner; let it wrap instead of forcing a page-wide scroll. */
  .beta { display: block; }

  .metric { grid-template-columns: 84px minmax(0, 1fr); }

  .net { padding: var(--s3); margin-top: var(--s5); }
  .net-stats { grid-template-columns: minmax(0, 1fr); row-gap: var(--s2); }

  /* The panel already reads as a break; 48px on top of it was a dead screen. */
  main > .h2:first-of-type { margin-top: 32px; }

  /* The chart is fluid, so it needs no scroller — just a tighter gap, or 60
     bars plus 59 gaps would leave the bars thinner than the space between. */
  .graph-track { gap: 1px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; animation: none !important; }
}




/* ---- guest / domain listings ---- */
.vis-cap { display: flex; justify-content: space-between; gap: 12px;
           margin-top: 14px; color: var(--fg3); text-transform: uppercase; }
.vis-list { list-style: none; margin-top: 4px;
            display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 0 24px; }
.vis-list li { display: flex; justify-content: space-between; gap: 12px;
               border-bottom: 1px solid var(--line); padding: 3px 0; }
.vis-list .d { color: var(--fg2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vis-list .n { color: var(--fg); font-variant-numeric: tabular-nums; flex: none; }
#pve { margin-top: 12px; }

@media (max-width: 640px) { .vis-list { grid-template-columns: 1fr; } }

/* A light touch only — the guest names carry the theme, the chrome stays plain. */
.orbit { margin-top: 10px; color: var(--fg3); }


/* ---- copy hardening ----
   Friction, not protection: the text stays in the HTML so crawlers and screen
   readers are unaffected, and anyone determined can still read source. Form
   fields stay selectable or they would be unusable. */
body { -webkit-user-select: none; user-select: none; }
.fi, input, textarea { -webkit-user-select: text; user-select: text; }

/* Honeypot: off-screen rather than display:none, which some bots skip. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- status ---- */
#status .lede { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.st-dot { width: 8px; height: 8px; flex: none;
          background: light-dark(oklch(58% 0.14 150), oklch(72% 0.15 150)); }
.st-dot.bad { background: light-dark(oklch(55% 0.19 27), oklch(68% 0.20 27)); }

/* Every row in every group is laid out by this one rule. The track sizes are
   fixed rather than `auto` on purpose: the grid lives on the <li>, so an auto
   track resolves per row from that row's own content — "202ms" and "1ms" would
   each size their column differently, which moved the 1fr name track's end and
   with it the X origin of the history bars. Fixed tracks make row geometry
   independent of the text inside it. */
.st-list {
  list-style: none;
  border-top: 1px solid var(--line);
  max-width: var(--content);

  /* 48 buckets of 3px with a 1px gap: 48*3 + 47*1. */
  --st-bar-w: 191px;
  --st-ms-w: 44px;      /* "202ms" is 5 chars = 36px; room for 6 */
  --st-state-w: 32px;   /* "down" is 4 chars = 28.8px */
  --st-col-gap: 16px;
}

.st-list li {
  display: grid;
  grid-template-columns:
    minmax(0, 1fr)          /* name — absorbs slack, never dictates it */
    var(--st-bar-w)         /* 24h history */
    var(--st-ms-w)          /* latency */
    var(--st-state-w);      /* up / down */
  column-gap: var(--st-col-gap);
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.st-name { color: var(--fg); min-width: 0;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-list li[data-down] .st-name { color: light-dark(oklch(55% 0.19 27), oklch(68% 0.20 27)); }

/* One <i> per 30-minute bucket, as equal grid tracks rather than fixed-width
   flex items — the bar then fills exactly --st-bar-w with no rounding drift,
   and collapses cleanly when it goes full-width on narrow screens. */
.st-bar { display: grid; grid-template-columns: repeat(48, 1fr); gap: 1px;
          width: var(--st-bar-w); height: 14px; }
.st-bar i { display: block; min-width: 0; height: 100%; }
.st-bar .ok   { background: light-dark(oklch(58% 0.14 150), oklch(72% 0.15 150)); }
.st-bar .warn { background: light-dark(oklch(72% 0.15 85),  oklch(80% 0.15 85)); }
.st-bar .bad  { background: light-dark(oklch(55% 0.19 27),  oklch(68% 0.20 27)); }

.st-ms { color: var(--fg3); text-align: right; white-space: nowrap;
         font-variant-numeric: tabular-nums; }
.st-state { text-transform: uppercase; color: var(--fg2);
            text-align: right; white-space: nowrap; }
.st-list li[data-down] .st-state { color: light-dark(oklch(55% 0.19 27), oklch(68% 0.20 27)); }

/* Narrow desktop / tablet: shrink the history to 2px buckets (48*2 + 47*1)
   rather than letting it crowd the names. Only the tokens change — the row
   definition above is untouched, so alignment holds. */
@media (max-width: 860px) {
  .st-list { --st-bar-w: 143px; --st-col-gap: 12px; }
}

/* Phone: the history drops to its own full-width line under the row. The 1fr
   buckets absorb whatever width is left, so there is no horizontal overflow.
   Every cell is placed explicitly — the bar comes before the latency in source
   order, and sparse auto-placement never moves the cursor backwards, so an
   implicitly placed latency would land on a third row instead of beside the
   name. */
@media (max-width: 640px) {
  .st-list li {
    grid-template-columns: minmax(0, 1fr) var(--st-ms-w) var(--st-state-w);
    row-gap: 6px;
  }
  .st-name  { grid-area: 1 / 1; }
  .st-ms    { grid-area: 1 / 2; }
  .st-state { grid-area: 1 / 3; }
  .st-bar   { grid-area: 2 / 1 / 3 / -1; width: 100%; }
}

/* Unobserved time: drawn so the bar is always a full day wide, but neutral so
   it never reads as uptime that was measured. */
.st-bar .none { background: var(--line); }
