/* trevware.com
   A portfolio set as a game menu. The work is one large list. Pointing at a row
   moves the cursor onto it and pulls a selection bar across the name; choosing a
   row opens its description in place, the way an item menu does. Every word on
   the site is set in the one pixel face. */

@font-face {
  font-family: 'Departure Mono';
  src: url('/fonts/DepartureMono.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

@view-transition { navigation: auto; }

/* Departure Mono is drawn on a 550-unit em with a 50-unit grid, so it is exactly
   crisp only at multiples of 11px. The name sizes stay on that grid. The small
   step is 16.5px, half a grid unit: exact at 2x, and off by half a device pixel
   at 3x where it cannot be seen. */
:root {
  color-scheme: light dark;
  interpolate-size: allow-keywords;

  --paper: #f6f2e9;
  --ink: #16130f;
  --dim: #6b645a;
  --gone: #8f877b;
  --accent: #c9391f;
  --on-accent: #f6f2e9;

  --mono: 'Departure Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --gut: 16px;
  --name: 33px;
  --meta: 16.5px;
  --small: 16.5px;
  --cur: 3px;
  --hang: calc(var(--cur) * 4 + 12px);
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);

  /* The cursor: a stepped triangle, seven rows tall, each row one --cur high. */
  --cursor: polygon(
    0 0, 25% 0, 25% 14.2857%, 50% 14.2857%, 50% 28.5714%, 75% 28.5714%,
    75% 42.8571%, 100% 42.8571%, 100% 57.1429%, 75% 57.1429%,
    75% 71.4286%, 50% 71.4286%, 50% 85.7143%, 25% 85.7143%, 25% 100%, 0 100%
  );
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #121110;
    --ink: #f1ece2;
    --dim: #a29b8f;
    --gone: #6f695f;
    --accent: #e2492f;
    --on-accent: #121110;
  }
}

/* The secret window colour, behind the Konami code: the blue of an old Final
   Fantasy window, with the cursor and bar in gold. */
:root[data-theme='ff'] {
  --paper: #0f1f6e;
  --ink: #ffffff;
  --dim: #aab6e8;
  --gone: #7482bd;
  --accent: #f2c14e;
  --on-accent: #0f1f6e;
}
:root[data-theme='ff'] body { background: linear-gradient(180deg, #1b2d8f, #070f3f) fixed; }

@media (min-width: 600px) { :root { --gut: 32px; --name: 44px; } }
@media (min-width: 900px) { :root { --gut: 40px; } }
@media (min-width: 1100px) { :root { --name: 55px; --meta: 22px; --cur: 4px; --hang: calc(var(--cur) * 4 + 16px); } }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--small)/26px var(--mono);
  -webkit-font-smoothing: none;
  -webkit-text-size-adjust: 100%;
}

a { color: inherit; text-decoration: none; }

/* Word pairs. Game words sit in .g and their plain partners in .m; retro shows
   .g, modern.css shows .m. Whole pieces that exist in one mode only are .g-only.
   The hidden one is display: none, so a screen reader reads only what shows. */
.m { display: none; }

/* The pixel face has one weight. A bold would be faked by smearing the glyphs. */
h1, h2, h3, h4, dt, b, strong { font-weight: 400; }


.skip {
  position: absolute; left: var(--gut); top: -100px;
  font: var(--small)/1 var(--mono); padding: 6px 8px;
  background: var(--accent); color: var(--on-accent);
}
.skip:focus { top: 12px; }

/* The page. One column on a phone; from 900px, the intro and the list sit side
   by side and the header lines up with them through a subgrid. */
.frame {
  min-height: 100svh;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto 1fr auto;
  padding: 18px var(--gut) 28px;
  padding-top: max(18px, env(safe-area-inset-top));
  padding-bottom: calc(28px + env(safe-area-inset-bottom));
  padding-left: max(var(--gut), env(safe-area-inset-left));
  padding-right: max(var(--gut), env(safe-area-inset-right));
}

@media (min-width: 900px) {
  .frame {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.6fr);
    grid-template-rows: auto 1fr;
    column-gap: 48px;
    padding-top: max(22px, env(safe-area-inset-top));
    padding-bottom: calc(36px + env(safe-area-inset-bottom));
  }
}

/* Header. Small, lowercase, and quiet, so the list carries the page. */
.top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 20px;
  font-size: var(--small);
  line-height: 22px;
  text-transform: lowercase;
}
.top nav, .top .contact { display: flex; flex-wrap: wrap; gap: 4px 20px; }
.top .here { color: var(--dim); }

/* On one column the switch sits at the right end of the name line and the links
   always take the line below. Left to wrap on their own, the links stay on the
   name line whenever they fit (around 700px) and run under the switch. */
@media (max-width: 899px) {
  .top { position: relative; }
  .top nav { flex-basis: 100%; }
  .top .mode { position: absolute; top: 0; right: 0; }
}

@media (min-width: 900px) {
  .top {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
  }
  .top nav { justify-content: space-between; }
}

/* The mode switch, drawn the retro way: a pixel slide switch with a square
   vermilion knob that moves in hard frames. modern.css redraws it round. */
.mode {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  line-height: inherit;
  text-transform: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.mode-track {
  position: relative;
  width: 30px;
  height: 16px;
  border: 2px solid var(--ink);
}
.mode-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 8px;
  height: 8px;
  background: var(--accent);
  transition: transform 120ms steps(3, end);
}
.mode[aria-checked='true'] .mode-knob { transform: translateX(16px); }
.mode:focus-visible { outline: none; }
.mode:focus-visible .mode-track { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Every small link reacts the way a row does: a bar snaps in behind it. */
.top a, .go, .stats a, .doc p a, .points a {
  background: linear-gradient(var(--accent), var(--accent)) no-repeat right / 0% 100%;
  transition: background-size 120ms steps(4, end), color 0s linear 60ms;
}
.top a { padding: 0 3px; margin: 0 -3px; }
/* In the header's grid the name is a grid item, and a grid item stretches to its
   column; the bar would then run the column's width. It hugs its text instead. */
.top > a { justify-self: start; }
.top a:hover, .top a:focus-visible, .top a:active, .go:active, .stats a:active,
.go:hover, .go:focus-visible,
.stats a:hover, .stats a:focus-visible,
.doc p a:hover, .doc p a:focus-visible,
.points a:hover, .points a:focus-visible {
  background-position: left;
  background-size: 100% 100%;
  color: var(--on-accent);
  box-shadow: none;
  outline: none;
}

/* The list. From 900px the cursor hangs in the gap to the left of the column, so
   the names line up with the header above them. */
.list {
  margin-top: 40px;
  padding-left: var(--hang);
}

@media (min-width: 900px) {
  .list {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin-top: clamp(48px, 16vh, 176px);
    margin-left: calc(var(--hang) * -1);
  }
}

.item { margin: 0; scroll-margin: 16px; }

.row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  padding: 4px 0;
  font-size: var(--name);
  line-height: 1.1;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  /* A row is a control, not text: clicking it quickly, twice or three times,
     must not paint a text selection across the name. */
  -webkit-user-select: none;
  user-select: none;
}
.row::-webkit-details-marker { display: none; }

/* On a phone the meta drops under the name; from 600px it takes the right edge,
   where a menu puts its values. */
@media (min-width: 600px) {
  .row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    column-gap: 24px;
  }
}

/* The selection bar. It is a background on the name itself, so it hugs the
   letters, clones onto each line when a long name wraps, and wipes in over four
   hard frames rather than easing. It grows from the left and leaves to the right.
   The name stays inline inside a .label cell, because a grid item is made a block
   and a block bar would fill the whole width on the lines it wraps to. */
.name {
  padding: 0 0.12em;
  margin-left: -0.12em;
  background: linear-gradient(var(--accent), var(--accent)) no-repeat right / 0% 100%;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size 140ms steps(4, end), color 0s linear 70ms;
}

.meta {
  display: flex;
  gap: 1.5em;
  margin-top: 2px;
  font-size: var(--meta);
  line-height: 1.4;
  color: var(--dim);
  white-space: nowrap;
  transition: color 0s linear 70ms;
}
/* Fixed columns, so every tag and every date lines up down the list whatever
   its length: the tag left in 3ch, the date right in 8ch (DELISTED is the widest). */
.meta .kind { width: 3ch; }
.meta .when { width: 8ch; text-align: right; }
@media (max-width: 599px) { .meta .kind, .meta .when { width: auto; text-align: left; } }

/* The cursor. Departure Mono has no U+25B6, so it is a stepped triangle drawn in
   clip-path: seven rows, each --cur tall, widening by one and back. It always
   holds its slot and only changes opacity, so a row never shifts. */
.row::before {
  content: '';
  position: absolute;
  left: calc(var(--hang) * -1);
  top: calc(4px + 0.52em - var(--cur) * 3.5);
  width: calc(var(--cur) * 4);
  height: calc(var(--cur) * 7);
  background: var(--accent);
  opacity: 0;
  clip-path: var(--cursor);
}

/* One cursor, whichever input spoke last: the script sets .kbd on the root when a
   key moves the cursor and clears it when the pointer moves, so a resting mouse
   never holds a second bar while the arrows move the first. */
/* Selected: pointed at (in the hover block below), pressed, focused, open, or parked. Pressed (:active) is
   what a touch screen shows under a finger before the row opens, the way an iOS
   cell highlights on touch; site.js adds the touchstart listener iOS needs before
   it will apply :active at all. Parked is the first row while nothing else is
   selected, because a menu never opens with no cursor on it. */
.row:active .name,
.row:focus-visible .name,
.item[open] > .row .name,
.list:not(:hover):not(:focus-within):not(:has(.item[open])) > .item:first-child > .row .name {
  background-position: left;
  background-size: 100% 100%;
  color: var(--on-accent);
  --sel: 1;
}

.row:active .meta,
.row:focus-visible .meta,
.item[open] > .row .meta,
.list:not(:hover):not(:focus-within):not(:has(.item[open])) > .item:first-child > .row .meta {
  color: var(--ink);
}

.row:active::before,
.row:focus-visible::before,
.item[open] > .row::before,
.list:not(:hover):not(:focus-within):not(:has(.item[open])) > .item:first-child > .row::before {
  opacity: 1;
  animation: bob 600ms steps(1, end) infinite;
  --sel: 1;
}

/* Pointed at, only where there is a real pointer. On a touch screen :hover sticks
   to the last touch point, and when an opening row moves the page, the browser
   lights whatever row now sits under it; there the finger selects through
   :active and the open state alone. */
@media (hover: hover) {
  :root:not(.kbd) .row:hover .name {
    background-position: left;
    background-size: 100% 100%;
    color: var(--on-accent);    --sel: 1;
  }
  :root:not(.kbd) .row:hover .meta { color: var(--ink); }
  :root:not(.kbd) .row:hover::before {
    opacity: 1;
    animation: bob 600ms steps(1, end) infinite;    --sel: 1;
  }
  :root:not(.kbd) .item.gone > .row:hover .name { color: var(--paper); }
}

/* An open row holds a still, greyed cursor, the way a game freezes the parent
   cursor while its submenu is up. The live cursor moves into the submenu. */
.item[open] > .row::before { animation: none; background: var(--dim); }

@keyframes bob {
  50% { transform: translateX(calc(var(--cur) * -1)); }
}

/* A delisted item answers the way a game does when you try one you cannot use:
   the name knocks side to side in hard frames, and the window says so. */
.item.gone[open] > .row .label { animation: bonk 180ms steps(3, end); }

@keyframes bonk {
  33% { transform: translateX(4px); }
  66% { transform: translateX(-4px); }
}

/* An item you can no longer use. The cursor still lands on it and it still has
   a description, but it greys out, and so does its bar. */
.item.gone .name { color: var(--gone); background-image: linear-gradient(var(--dim), var(--dim)); }
.item.gone > .row::before { background: var(--dim); }
.item.gone > .row:focus-visible .name,
.item.gone[open] > .row .name { color: var(--paper); }

/* THE SUBMENU. Opening a row raises a game window under it, in one staged run of
   hard frames, about half a second end to end:

     0ms    the parent cursor greys and stops; the space opens in three frames
            while a line draws out from the cursor
     90ms   the window unfolds up and down from that line
     180ms  the item's icon pops in with four sparkles; the description lands
     250ms  the stats land one by one and their numbers count up (site.js)
     450ms  the headline number flashes once
     400ms  the link lands and a second cursor starts to bob on it

   Closing runs it backwards, quieter and faster: the contents go, the window
   folds to its line, the line runs back into the cursor, the space shuts. The
   script holds the row open with .closing for that quarter second. */
.item::details-content {
  block-size: 0;
  overflow: clip;
  transition: block-size 90ms steps(3, end), content-visibility 90ms allow-discrete;
}
.item[open]::details-content { block-size: auto; }
.item.closing::details-content { block-size: 0; transition-delay: 180ms; transition-duration: 60ms; }

.panel {
  position: relative;
  max-width: 40rem;
  padding: 10px 0 26px;
}

/* The line the window unfolds from, drawn on the panel so the window's own clip
   cannot hide it. It sits on the window's middle: the panel's top padding plus
   half of what is left after both paddings. */
.panel::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: calc(10px + (100% - 36px) / 2 - 1px);
  height: 2px;
  background: var(--ink);
  transform-origin: left;
  opacity: 0;
}
.item[open] .panel::before { animation: line-out 180ms both; }
.item.closing .panel::before { animation: line-in 90ms steps(3, end) 110ms both; }

@keyframes line-out {
  0% { opacity: 1; transform: scaleX(0); animation-timing-function: steps(3, end); }
  50% { opacity: 1; transform: scaleX(1); animation-timing-function: step-end; }
  100% { opacity: 0; transform: scaleX(1); }
}
@keyframes line-in {
  from { opacity: 1; transform: scaleX(1); }
  to { opacity: 1; transform: scaleX(0); }
}

.sub {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 14px 16px;
  margin: 0 4px;
  padding: 14px 16px 16px;
  background: var(--paper);
}
.sub > :not(.icon-wrap, .desc) { grid-column: 1 / -1; }
.sub p { margin: 0; }

.item[open] .sub { animation: unfold 90ms steps(4, end) 90ms both; }
.item.closing .sub { animation: fold 110ms steps(3, end) both; }
.item.closing .sub > * { opacity: 0; animation: none; }

.warn { text-transform: uppercase; }
.item[open] .warn { animation: land 120ms steps(2, end) 180ms both; }
.item[open] .desc { animation: land 120ms steps(2, end) 180ms both; }

/* The item icon: 16 by 16 pixels at a whole-pixel scale, ink and accent. It pops
   in over three held frames, and four sparkles leave its corners and go out at
   1.35 times its half-width, then vanish. */
.icon-wrap {
  --ip: 2px;
  --r: calc(var(--ip) * 8);
  position: relative;
  display: block;
  width: calc(var(--ip) * 16);
  height: calc(var(--ip) * 16);
}
@media (min-width: 1100px) { .icon-wrap { --ip: 3px; } }
.icon { display: block; width: 100%; height: 100%; }
.icon .ink { fill: var(--ink); }
.icon .hue { fill: var(--accent); }
.item.gone .icon .ink, .item.gone .icon .hue { fill: var(--gone); }

.icon-wrap::after {
  content: '';
  position: absolute;
  left: calc(50% - var(--ip));
  top: calc(50% - var(--ip));
  width: calc(var(--ip) * 2);
  height: calc(var(--ip) * 2);
  opacity: 0;
}
.item[open] .icon { animation: pop 120ms 180ms both; }
.item[open] .icon-wrap::after { animation: sparkle 180ms steps(3, end) 200ms both; }
.item.gone[open] .icon-wrap::after { animation: none; }

@keyframes pop {
  0% { transform: scale(0.4); animation-timing-function: step-end; }
  33% { transform: scale(1.35); animation-timing-function: step-end; }
  66%, 100% { transform: scale(1); }
}
@keyframes sparkle {
  from {
    opacity: 1;
    box-shadow:
      calc(var(--r) * -0.8) calc(var(--r) * -0.8) var(--accent), calc(var(--r) * 0.8) calc(var(--r) * -0.8) var(--accent),
      calc(var(--r) * -0.8) calc(var(--r) * 0.8) var(--accent), calc(var(--r) * 0.8) calc(var(--r) * 0.8) var(--accent);
  }
  to {
    opacity: 0;
    box-shadow:
      calc(var(--r) * -1.35) calc(var(--r) * -1.35) var(--accent), calc(var(--r) * 1.35) calc(var(--r) * -1.35) var(--accent),
      calc(var(--r) * -1.35) calc(var(--r) * 1.35) var(--accent), calc(var(--r) * 1.35) calc(var(--r) * 1.35) var(--accent);
  }
}

/* The stats, the way a status screen lists them: label left, value right. The
   rows land one by one; the script counts the numbers up under them. */
.tally { margin: 0; }
.tally div { display: flex; justify-content: space-between; gap: 16px; }
.tally dt { color: var(--dim); text-transform: uppercase; }
.tally dd { margin: 0; padding: 0 3px; margin-right: -3px; text-align: right; text-transform: uppercase; }
.item[open] .tally div { animation: land 120ms steps(2, end) both; }
.item[open] .tally div:nth-child(1) { animation-delay: 250ms; }
.item[open] .tally div:nth-child(2) { animation-delay: 280ms; }
.item[open] .tally div:nth-child(3) { animation-delay: 310ms; }
.item[open] .tally div:nth-child(4) { animation-delay: 340ms; }
.tally .flash { animation: flash 240ms both; }

@keyframes flash {
  0%, 49% { background: var(--accent); color: var(--on-accent); }
  50%, 100% { background: transparent; color: inherit; }
}

/* The link, and the submenu's own cursor on it. The cursor is the small size,
   two pixels a step, and lands last, once everything else is up. */
.go {
  --cs: 2px;
  position: relative;
  justify-self: start;
  margin-left: calc(var(--cs) * 4 + 10px);
  padding: 0 4px;
  font-size: var(--small);
  line-height: 24px;
  text-transform: uppercase;
  box-shadow: inset 0 -2px 0 var(--accent);
}
.go::before {
  content: '';
  position: absolute;
  left: calc((var(--cs) * 4 + 10px) * -1);
  top: calc(50% - var(--cs) * 3.5);
  width: calc(var(--cs) * 4);
  height: calc(var(--cs) * 7);
  background: var(--accent);
  clip-path: var(--cursor);
}
.item[open] .go { animation: land 120ms steps(2, end) 400ms both; }
.item[open] .go::before { animation: nudge 600ms steps(1, end) 520ms infinite; }

@keyframes nudge {
  50% { transform: translateX(calc(var(--cs) * -1)); }
}

/* A game window: a double line drawn as a border plus an outline, so it stays on
   whole pixels. Used for the party window and for messages. */
.window {
  border: 2px solid var(--ink);
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* A message window, the one-line kind a game raises when something happens. */
.message {
  width: fit-content;
  max-width: 100%;
  margin: 4px 4px 0;
  padding: 6px 12px;
  text-transform: uppercase;
}

/* The party window sits where the Final Fantasy menu keeps its time and gil box,
   and carries the join message a game shows when someone enters the party. */
/* In one column the party window sits square in the page, the same gutter on both
   sides (the 4px is its outer line). In two columns it is a caption-sized box at
   the foot of the left column. */
.party {
  margin: 56px 4px 4px;
  padding: 12px 14px 14px;
}

@media (min-width: 900px) {
  .party {
    grid-column: 1;
    grid-row: 2;
    align-self: end;
    margin: 0 4px 4px;
    max-width: 34ch;
  }
}

.joined { margin: 0; text-transform: uppercase; }

.stats { margin: 10px 0 0; }
.stats div { display: flex; justify-content: space-between; gap: 16px; }
.stats dt { color: var(--dim); text-transform: uppercase; }
.stats dd { margin: 0; text-align: right; text-transform: uppercase; }
.stats .split { margin-top: 10px; padding-top: 10px; border-top: 2px solid var(--ink); }
.stats a { box-shadow: inset 0 -2px 0 var(--accent); padding: 0 1px; margin: 0 -1px; }

/* The window opens the way a game window does, unfolding from its middle line
   in hard frames once the list has landed; then the message shows, then the
   stats drop in one row at a time. About half a second end to end. */
.party { animation: unfold 160ms steps(4, end) 260ms both; }
.joined { animation: land 120ms steps(2, end) 420ms both; }
.stats div { animation: land 120ms steps(2, end) both; }
.stats div:nth-child(1) { animation-delay: 470ms; }
.stats div:nth-child(2) { animation-delay: 505ms; }
.stats div:nth-child(3) { animation-delay: 540ms; }
.stats div:nth-child(4) { animation-delay: 575ms; }
.stats div:nth-child(5) { animation-delay: 610ms; }
.stats div:nth-child(6) { animation-delay: 645ms; }
.stats div:nth-child(7) { animation-delay: 680ms; }
.stats div:nth-child(8) { animation-delay: 715ms; }
.stats div:nth-child(9) { animation-delay: 750ms; }

/* The EXP bar: twelve cells, one for each month since the last work anniversary
   (1 July), so it fills a cell a month and tops out the day LV goes up. The cells
   are separate elements with a 2px gap, so every edge falls on a whole pixel.
   Once the party window is up, the lit cells fill in one after another in hard
   frames, the way a bar tallies after a battle. */
.stats .xp-row { align-items: center; }
.stats .xp-row dd { flex: 1; display: flex; }
.xp {
  flex: 1;
  display: flex;
  gap: 2px;
  height: 14px;
  margin-left: 24px;
  padding: 2px;
  border: 2px solid var(--ink);
}
.xp i { flex: 1; background: color-mix(in srgb, var(--ink) 12%, transparent); }
.xp i.on { background: var(--accent); animation: cell 1ms both; }
@keyframes cell { from { background: color-mix(in srgb, var(--ink) 12%, transparent); } }
.xp i:nth-child(1) { animation-delay: 760ms; }
.xp i:nth-child(2) { animation-delay: 805ms; }
.xp i:nth-child(3) { animation-delay: 850ms; }
.xp i:nth-child(4) { animation-delay: 895ms; }
.xp i:nth-child(5) { animation-delay: 940ms; }
.xp i:nth-child(6) { animation-delay: 985ms; }
.xp i:nth-child(7) { animation-delay: 1030ms; }
.xp i:nth-child(8) { animation-delay: 1075ms; }
.xp i:nth-child(9) { animation-delay: 1120ms; }
.xp i:nth-child(10) { animation-delay: 1165ms; }
.xp i:nth-child(11) { animation-delay: 1210ms; }
.xp i:nth-child(12) { animation-delay: 1255ms; }


@keyframes unfold {
  from { clip-path: inset(50% -8px 50% -8px); }
  to { clip-path: inset(-8px -8px -8px -8px); }
}

/* The party window reacts to the list. With an ENG row under the cursor the class
   and subclass trade places, sliding past each other in hard frames. */
.stats dd { transition: transform 120ms steps(3, end); }
.stats.swapped > div:nth-child(1) dd { transform: translateY(26px); }
.stats.swapped > div:nth-child(2) dd { transform: translateY(-26px); }

/* Pop-up windows raised by the script: the toast for the secret, and the
   "Continue?" countdown after a minute of nothing. Both unfold like the party
   window and sit on the paper so the page does not show through. */
.toast, .continue {
  position: fixed;
  z-index: 10;
  background: var(--paper);
  animation: unfold 160ms steps(4, end) both;
}
.toast {
  left: 50%;
  bottom: calc(28px + env(safe-area-inset-bottom));
  translate: -50% 0;
}
.toast.leaving, .continue.leaving { animation: fold 120ms steps(3, end) both; }

.continue {
  right: max(var(--gut), env(safe-area-inset-right));
  top: calc(64px + env(safe-area-inset-top));
  display: grid;
  justify-items: center;
  gap: 4px;
  min-width: 14ch;
  padding: 12px 18px 14px;
  text-transform: uppercase;
}
.continue-count { font-size: var(--name); line-height: 1; color: var(--accent); }
.continue.over .continue-count { font-size: inherit; line-height: inherit; color: var(--dim); }

@keyframes fold {
  to { clip-path: inset(50% -8px 50% -8px); }
}

/* The controls legend, the line of button hints at the foot of a game screen.
   Only where there is a keyboard to press, which a fine pointer stands in for. */
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 24px;
  margin: 40px 0 0;
  padding-left: var(--hang);
  color: var(--dim);
  text-transform: uppercase;
}
.legend kbd {
  margin-right: 8px;
  padding: 0 4px;
  border: 2px solid currentColor;
  font: inherit;
}
@media not all and (hover: hover) and (pointer: fine) { .legend { display: none; } }
@media (min-width: 900px) {
  .frame { grid-template-rows: auto 1fr auto; }
  .legend { grid-column: 2; grid-row: 3; margin-top: 32px; padding-left: 0; }
  .party { grid-row: 2 / 4; }
}
.legend { animation: land 180ms steps(2, end) 260ms both; }

/* The page loads the way a menu draws itself: the rows land one after another
   in two hard frames each, then the cursor parks on the first. */
.list > .item { animation: land 180ms steps(2, end) both; }
.list > .item:nth-child(2) { animation-delay: 45ms; }
.list > .item:nth-child(3) { animation-delay: 90ms; }
.list > .item:nth-child(4) { animation-delay: 135ms; }
.list > .item:nth-child(5) { animation-delay: 180ms; }
.list > .item:nth-child(6) { animation-delay: 225ms; }

@keyframes land {
  from { opacity: 0; transform: translateX(-8px); }
}

/* Documents: the resume and the 404 page. The text column lines up with the list
   column on the home page, so moving between them does not jump. */
.doc {
  margin-top: 40px;
  max-width: 40rem;
}
@media (min-width: 900px) {
  .doc { grid-column: 2; grid-row: 2; margin-top: clamp(48px, 12vh, 132px); }
}

.doc h1 {
  margin: 0;
  font-size: var(--name);
  line-height: 1.1;
  text-transform: uppercase;
}
.dateline {
  margin: 6px 0 0;
  font-size: var(--small);
  line-height: 22px;
  color: var(--dim);
  text-transform: uppercase;
}
/* Resume hierarchy: a ruled section label, then the company with the accent
   square, then the role and its dates, then the points. */
.doc h2 {
  margin: 64px 0 0;
  padding-top: 10px;
  border-top: 2px solid var(--ink);
  font-size: var(--small);
  line-height: 22px;
  color: var(--dim);
  text-transform: uppercase;
}
.doc h3 {
  margin: 28px 0 0;
  font-weight: 400;
  font-size: var(--meta);
  line-height: 1.3;
  text-transform: uppercase;
}
.doc h2 + h3 { margin-top: 20px; }
.doc h3::before {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: 10px;
  vertical-align: 0.2em;
  background: var(--accent);
}
.doc h4.dateline { margin-top: 4px; }
.nowrap { white-space: nowrap; }
.doc ul + h4.dateline { margin-top: 20px; }
.doc p { margin: 12px 0 0; }
.doc .lede { margin-top: 28px; }

.points { margin: 10px 0 0; padding: 0; list-style: none; }
.points li {
  position: relative;
  margin-top: 8px;
  padding-left: 20px;
}
.points li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 10px;
  width: 6px;
  height: 6px;
  background: var(--dim);
}

.skills { display: grid; gap: 20px 40px; margin: 20px 0 0; }
@media (min-width: 700px) { .skills { grid-template-columns: 1fr 1fr; } }
.skills dt { text-transform: uppercase; }
.skills dd { margin: 4px 0 0; }

.doc .list { margin-top: 36px; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Class history: the job changes, newest first, with the current class at the top
   under the cursor and its bar. */
.classes { margin: 20px 0 0; padding: 0 0 0 var(--hang); list-style: none; }
@media (min-width: 900px) { .classes { margin-left: calc(var(--hang) * -1); } }
.classes li {
  position: relative;
  display: grid;
  grid-template-columns: 5ch minmax(0, 1fr) auto;
  gap: 16px;
  text-transform: uppercase;
}
.classes .year, .classes .guild { color: var(--dim); }
.classes .now .name {
  padding: 0 4px;
  margin-left: -4px;
  background: var(--accent);
  color: var(--on-accent);
  transition: none;
}
.classes .now .guild { color: var(--ink); }
.classes .now::before {
  content: '';
  position: absolute;
  left: calc(var(--hang) * -1);
  top: calc(13px - var(--cur) * 3.5);
  width: calc(var(--cur) * 4);
  height: calc(var(--cur) * 7);
  background: var(--accent);
  clip-path: var(--cursor);
  animation: bob 600ms steps(1, end) infinite;
}

/* Equipment: the skills, each group in the slot a game would equip it to. */
.skills .slot { display: block; color: var(--dim); }

/* The 404 encounter: the message, then FIGHT and RUN under the cursor. */
.encounter .message { margin-top: 8px; }
.encounter .list { margin-top: 28px; }

/* Touch: the small mono links get a finger-sized hit area without changing how
   they look, since a 22px line is well under the 44px a finger needs. */
@media (pointer: coarse) {
  /* A row answers only where there is something to press: the name and its tag
     and date. The rest of its width passes touches through, so a thumb that
     lands in the empty space to scroll just scrolls. The name is inline and hugs
     its letters; the meta is told to hug its own. Focus is untouched, so the
     keyboard still reaches the whole row. */
  .row { pointer-events: none; }
  .row .name, .row .meta { pointer-events: auto; }
  .row .meta { justify-self: start; }
  @media (min-width: 600px) { .row .meta { justify-self: end; } }

  /* The press highlight waits 100ms, the way an iOS cell does. A touch that turns
     into a scroll loses :active before then, so it never lights anything. */
  .row:active .name { transition-delay: 100ms, 170ms; }
  .row:active .meta { transition-delay: 170ms; }
  .row:active::before { transition: opacity 0s linear 100ms; }

  .top a, .go, .stats a, .doc p a, .points a { position: relative; }
  .top a::after, .go::after, .stats a::after, .doc p a::after, .points a::after {
    content: '';
    position: absolute;
    inset: -11px -6px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after, .item::details-content {
    animation: none !important;
    transition: none !important;
  }
}

@media print {
  .top nav, .skip { display: none; }
  :root { --paper: #fff; --ink: #000; --dim: #444; }
  .frame { min-height: 0; display: block; padding: 0; }
  .doc { margin: 24px 0 0; max-width: none; }
  body { font-size: 11pt; line-height: 1.5; }
}
