/* stevenpenza.com — Operator's Ledger
   Tokens lifted from the shotvector palette; the four project hues are the
   validated categorical set (OKLCH-spaced, CVD-checked), not hand-picked. */

:root {
  --bg:        #0a0b0c;
  --panel:     #121417;
  --sunk:      #0e1114;
  --line:      #262b31;
  --line-soft: #1b1f24;
  --line-2:    #333940;
  --txt:       #e6e4df;
  --txt-2:     #9ba1a8;
  --txt-3:     #646a71;
  --txt-4:     #4b5158;
  --amber:     #e3a53c;

  /* project identity — direct-labelled everywhere, never colour alone */
  --photo: #ba5db3;
  --coin:  #d16022;
  --file:  #2389e2;
  --shot:  #029e72;

  --ob:    #2f8f66;   /* obfuscated value */

  --sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --pad: 56px;
}

@media (max-width: 900px) { :root { --pad: 24px; } }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--txt);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--amber); text-decoration: none; }
a:hover { color: #f0bd63; }

.wrap { max-width: 1440px; margin: 0 auto; padding-bottom: 56px; }

/* ---------- header ---------- */
.top {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  padding: 18px var(--pad); border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: baseline; gap: 20px; }
.brand b { font-family: var(--mono); font-size: 13px; letter-spacing: 0.08em; font-weight: 500; }
.brand span { font-family: var(--mono); font-size: 11px; color: var(--txt-3); }
.topright { display: flex; align-items: center; gap: 22px; font-family: var(--mono); font-size: 11px; color: var(--txt-3); }

.authbtn {
  display: flex; align-items: center; gap: 8px; min-height: 44px;
  border: 1px solid var(--line-2); background: none; padding: 8px 14px;
  color: var(--txt); font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; cursor: pointer;
}
.authbtn:hover { border-color: var(--txt-3); }
body[data-auth="in"] .authbtn { border-color: #2a4a3a; background: #101a15; color: #46d495; }

/* ---------- summary strip ---------- */
.summary {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1px;
  background: var(--line); border-bottom: 1px solid var(--line);
}
.summary > div { background: var(--bg); padding: 22px 24px; }
.summary dt, .rowmetrics dt, .key {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em; color: var(--txt-3); margin: 0;
}
.summary dd { margin: 0 0 9px; font-size: 31px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
.summary .sub { color: var(--txt-4); font-size: 20px; }

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

/* ---------- system list ---------- */
.colhead, .row {
  display: grid; grid-template-columns: 132px 250px 118px minmax(0, 1fr) 132px; gap: 26px;
  padding: 24px var(--pad); background: var(--bg);
}

/* Two rows: the name and the state pill sit on the first, and the stack runs
   beneath them across BOTH their columns. The stack is the tallest thing in a
   row and the state pill is two words, so keeping them in one column each wasted
   the space under the pill and squeezed the stack into 250px. */
.row {
  /* Both tracks share the slack. A row can never be shorter than its 107px
     thumbnail, so a short stack leaves space over — with min-content on the
     first track all of it fell below the stack, which read as a row that had
     run out of things to say. Split between the tracks, with the name sitting
     at the bottom of its cell and the stack at the top of its, the two hug and
     the leftover sits outside them. Rows whose text already exceeds 107px are
     unaffected: content wins over 1fr. */
  /* Title track sized to the title, stack track takes the remainder, and every
     cell starts at the top — so the title's cap-line meets the top of the
     thumbnail and the trend, and the whole row reads off one line across all
     five columns. On a row whose text is shorter than the 107px thumbnail the
     leftover falls below the stack; that is the cost of aligning to the top,
     and it is the alignment that carries the row. */
  grid-template-rows: min-content 1fr;
  row-gap: 6px;
  align-items: start;
}
.row > .thumb { grid-column: 1; grid-row: 1 / span 2; }
.row > .sys   { grid-column: 2; grid-row: 1; }
.row > .state { grid-column: 3; grid-row: 1; }
.row > .stack { grid-column: 2 / 4; grid-row: 2; }
.row > .mid   { grid-column: 4; grid-row: 1 / span 2; }
.row > .trend { grid-column: 5; grid-row: 1 / span 2; }

/* Framed like the trend, so a row reads as two small screens with the numbers
   between them. */
.thumb {
  display: block; border: 1px solid var(--line); background: #0d1013;
  text-decoration: none; overflow: hidden;
}
.thumb img {
  display: block; width: 100%; height: 82px;
  object-fit: cover; object-position: top center;   /* keep the hero, drop the fold */
}
.thumb span {
  display: block; padding: 5px 7px; border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.06em; color: var(--txt-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.thumb:hover { border-color: var(--txt-4); }
a.thumb:hover span { color: var(--amber); }
.thumb.none {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; height: 105px; color: var(--txt-4);
}
.thumb.none svg { width: 18px; height: 18px; }
.thumb.none span { border: 0; padding: 0; }
.colhead {
  padding: 16px var(--pad) 11px; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: 0.12em; color: var(--txt-4); border-bottom: 1px solid var(--line);
}
.row { border-bottom: 1px solid var(--line-soft); }   /* alignment set above; centering here overrode it */
.row:last-of-type { border-bottom: 1px solid var(--line); }

.sys { display: flex; align-items: flex-start; gap: 12px; }
.swatch { width: 10px; height: 10px; margin-top: 6px; flex-shrink: 0; }
.name { font-size: 19px; font-weight: 600; line-height: 1.16; text-wrap: pretty; }
.lockup { display: flex; align-items: center; gap: 7px; line-height: 1.2; flex-wrap: wrap; }
.lockup .word { font-size: 19px; font-weight: 600; letter-spacing: -0.015em; }
.badge {
  font-family: var(--mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.14em;
  border-radius: 2px; padding: 2px 4px 1px 5px;
}
.badge.rf { color: var(--coin); border: 1px solid var(--coin); }
.badge.ai { color: var(--bg); background: #34c39c; }

.stack { display: flex; flex-direction: column; gap: 2px; }
.stack div { font-family: var(--mono); font-size: 10.5px; color: var(--txt-3); }
/* The footnote spans the whole row it annotates, on a third track below
   everything else — a step dimmer than the tiers so it reads as a note, and
   full width so it belongs to the row rather than to the stack cell. No rule
   above it: the row already ends in a border, and two lines that close to each
   other read as a mistake. */
.row > .note-def {
  grid-column: 1 / -1; grid-row: 3;
  margin-top: 12px;
  font-family: var(--mono); font-size: 10.5px; color: var(--txt-4);
}
.stack .tier { color: var(--txt-4); letter-spacing: 0.1em; }

.state { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 11px; padding-top: 3px; }
.state.ok   { color: var(--txt); }
.state.warn { color: var(--amber); }
.state.idle { color: var(--txt-3); }

.rowmetrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
.rowmetrics dd { margin: 0; font-size: 19px; font-weight: 600; font-variant-numeric: tabular-nums; }
.rowmetrics dt { font-size: 10px; margin-top: 3px; }
.rowmetrics .unit { font-size: 12px; color: var(--txt-3); }

/* ---------- suite meter ---------- */
.suites { margin-top: 15px; }
.bars { display: flex; gap: 2px; margin-bottom: 9px; }
.bars i { height: 8px; display: block; transition: flex-grow .5s ease-in-out; }  /* duration re-rolled per tick in app.js */
.keys { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.keys > span { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 10px; color: var(--txt-2); }
.keys em { width: 8px; height: 8px; flex-shrink: 0; }
.keys b { color: var(--txt-3); font-weight: 400; font-variant-numeric: tabular-nums; }
.note { display: flex; align-items: center; gap: 5px; margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--txt-3); }
.note.warn { color: var(--amber); }

/* Same box as .thumb — border, ground, caption inside on its own rule — so the
   two ends of a row read as a matched pair rather than two unrelated widgets. */
.trend {
  margin: 0; width: 132px; justify-self: end;
  border: 1px solid var(--line); background: #0d1013; overflow: hidden;
}
/* the trend reads as a small screen, which is what makes the static land */
.trend svg { display: block; width: 100%; height: 82px; }
.trend figcaption {
  padding: 5px 7px; border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.06em; color: var(--txt-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 1180px) {
  .colhead { display: none; }
  /* the thumbnail keeps its place beside the name; everything else goes full width */
  .row { grid-template-columns: 132px minmax(0, 1fr); grid-template-rows: none; gap: 18px; }
  .row > .thumb { grid-column: 1; grid-row: auto; }
  .row > .sys { grid-column: 2; grid-row: auto; }
  .row > :not(.thumb):not(.sys) { grid-column: 1 / -1; grid-row: auto; }
  .trend { justify-self: start; }
}
@media (max-width: 620px) { .rowmetrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---------- obfuscated metrics ---------- */
.metric { font-variant-numeric: tabular-nums; }
.metric[data-state="hidden"] {
  display: inline-block; position: relative; color: var(--ob); font-family: var(--mono);
}
/* The new character rides up into its slot and overshoots slightly before
   settling. Only the glyph that changed moves — putting this on the whole value
   would read as the entire field turning over, which is the bug it replaced.
   No clipping: the travel is under a line-height and the glyph fades in, so it
   never reads as text escaping its row. */
.metric i { font-style: normal; display: inline-block; }
.metric i.bit { animation: glyphIn 380ms cubic-bezier(0.22, 0.9, 0.24, 1) 1; }

@keyframes glyphIn {
  0%   { transform: translateY(0.75em); opacity: 0; }
  40%  { opacity: 1; }
  58%  { transform: translateY(-0.20em); }   /* past the mark */
  80%  { transform: translateY(0.07em); }    /* and a little under */
  100% { transform: translateY(0); opacity: 1; }
}

/* ---------- reach out ---------- */
.reach { margin: 42px var(--pad) 0; background: var(--panel); border: 1px solid var(--line); padding: 34px 38px; }
.reach .grid { display: grid; grid-template-columns: 350px minmax(0, 1fr); gap: 52px; align-items: start; }
.reach h2 { font-size: 23px; font-weight: 600; margin: 0 0 12px; }
.reach p { font-size: 13.5px; color: var(--txt-2); line-height: 1.62; margin: 0 0 16px; }
.reach .links { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; font-family: var(--mono); font-size: 11px; color: var(--txt-3); }
.linkbtn {
  display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 6px 0;
  background: none; border: 0; cursor: pointer;
  font-family: var(--mono); font-size: 11px; color: var(--amber);
}
.linkbtn:hover { color: #f0bd63; }
body[data-auth="out"] .linkbtn { color: var(--txt-3); }
body[data-auth="out"] .linkbtn:hover { color: var(--txt-2); }
body[data-auth="in"] #resumelock { display: none; }
/* Visitors get the résumé from the checkbox on the form; a second control here
   would only be a dead end for them. */
body[data-auth="out"] #resume,
body[data-auth="out"] #resume + span { display: none; }   /* its separator goes too */

.check { display: flex; align-items: center; gap: 9px; cursor: pointer; min-height: 32px; }
.check input { width: 15px; height: 15px; accent-color: var(--amber); cursor: pointer; }
.check span { font-family: var(--mono); font-size: 11.5px; color: var(--txt-2); letter-spacing: 0.04em; }
.check:hover span { color: var(--txt); }

/* flex-direction is explicit: the bare `form` rule above sets column, and in a
   column container a flex-basis sizes HEIGHT — which stretched this input to 190px. */
@media (max-width: 900px) { .reach .grid { grid-template-columns: 1fr; gap: 28px; } .reach { padding: 26px 22px; } }

form { display: flex; flex-direction: column; gap: 13px; }
select {
  width: 100%; min-height: 46px; background: var(--sunk); border: 1px solid var(--line);
  color: var(--txt); font-family: var(--mono); font-size: 12px; letter-spacing: 0.06em;
  padding: 0 36px 0 14px; cursor: pointer;
  /* the native arrow is the one control the palette cannot reach, so it is drawn */
  appearance: none; -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--txt-3) 50%),
                    linear-gradient(135deg, var(--txt-3) 50%, transparent 50%);
  background-position: calc(100% - 19px) 21px, calc(100% - 14px) 21px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select:hover { border-color: var(--line-2); }
select:focus { outline: none; border-color: var(--txt-3); }
.pair { display: flex; gap: 10px; flex-wrap: wrap; }
.pair > * { flex: 1 1 200px; }
input[type="text"], input[type="email"], textarea {
  width: 100%; min-height: 46px; background: var(--sunk); border: 1px solid var(--line);
  color: var(--txt); font-family: var(--sans); font-size: 13px; padding: 13px 14px;
}
textarea { min-height: 92px; resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--txt-4); }
input:focus, textarea:focus { outline: none; border-color: var(--txt-3); }
button[type="submit"] {
  align-self: flex-end; min-height: 46px; background: var(--amber); color: var(--bg);
  border: 0; padding: 0 30px; font-family: var(--sans); font-weight: 600; font-size: 13.5px; cursor: pointer;
}
button[type="submit"]:hover { background: #f0bd63; }
button[type="submit"][disabled] { opacity: .5; cursor: default; }
.formnote { font-family: var(--mono); font-size: 11px; color: var(--txt-3); }
.formnote.bad { color: #e5544b; }

.foot {
  padding: 22px var(--pad) 0; font-family: var(--mono); font-size: 10.5px; color: var(--txt-4);
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
}

/* !important because this must beat whatever the element would otherwise get:
 * the honeypot is an input[type="text"] (0,1,1), which outranks this class
 * (0,1,0) and was giving it width:100% — 1440px, absolutely positioned, pushing
 * ~500px of horizontal scroll onto every page. Still invisible via clip-path,
 * so it was a layout bug rather than a leak, and invisible in the one axis
 * nobody checks. A hiding utility that can be outvoted is not a hiding utility. */
.visually-hidden {
  position: absolute !important; width: 1px !important; height: 1px !important;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; padding: 0 !important; border: 0 !important;
}

@media (prefers-reduced-motion: reduce) {
  /* JS also checks this and stops re-rolling; these cover the CSS half. */
  .metric i.bit { animation: none; }
  .bars i { transition: none; }
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
