/* core/platform.css — the look of every screen in every experiment.
 *
 * Light throughout. A dark interface that hands over to a white stimulus
 * surface puts a large luminance step in front of the participant a moment
 * before a brief, masked target — bad for the participant's adaptation state
 * and confusing to run. Everything here is white, so nothing changes when the
 * stimulus appears.
 *
 * Stimulus appearance is deliberately not set here: anything a participant is
 * asked to discriminate belongs to that experiment's own stylesheet.
 */

:root {
  --bg: #ffffff;
  --panel: #f5f6f8;
  --fg: #14181d;
  --dim: #626c7a;
  --line: #e2e5ea;
  --accent: #1a5fd0;
  --orange: #d86f33;          /* buttons and the progress bar */
  --orange-dark: #bf5d26;
  --ok: #1a7f4b;
  --bad: #c0342b;
  --warn: #9a6400;
  --stage-bg: #ffffff;
  --stage-fg: #000000;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Response-grid cell height. Height is the binding constraint — seven rows
     plus a prompt and a hint have to fit a tablet in landscape — while the
     width is taken care of by the grid's own columns, which span the screen. */
  --psy-cell-h: min(7.8vh, 6vw);
}

/* A laptop in a browser window, rather than a tablet held in landscape: less
   height to spend, and the prompt and the hint are not the parts to lose. */
@media (max-height: 720px) {
  :root { --psy-cell-h: min(7vh, 5.2vw); }
}

* { box-sizing: border-box; }

/* Nothing scrolls, nothing zooms, nothing rubber-bands. A stray two-finger
   gesture mid-trial would move the stimulus off its calibrated eccentricity,
   and a pinch would change the visual angle of everything on screen — the one
   quantity the whole design depends on. */
html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
  -ms-touch-action: none;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body.psy {
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.55 ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

/* The operator panels are the one place that may scroll — a diagnostics
   report is longer than a screen. */
#psy-panel { touch-action: pan-y; }
.psy-panel-body, .psy-panel-body * { -webkit-user-select: text; user-select: text; }

/* Exactly the height of the viewport, not "at least". With `min-height`, a tall
   summary made this element grow, so #psy-screen was always as tall as its own
   content and had nothing to scroll — the overflow moved up to <body>, which
   clips it. A fixed height gives the screen a bounded box to scroll inside. */
#psy-app { height: 100%; display: flex; flex-direction: column; background: var(--bg); }

/* ---------- screens ---------- */
/* The page itself never scrolls — a stray drag mid-trial would move the
   stimulus off its calibrated eccentricity. The *screen* may, because the
   end-of-session summary is a table per condition plus a row per character and
   is taller than a tablet. Vertically centred while it fits, scrolled from the
   top once it does not. */
#psy-screen {
  display: none;
  flex: 1;
  min-height: 0;              /* a flex child will not shrink below its content without this */
  align-items: center;
  justify-content: center;
  padding: 40px 24px 64px;
  overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
}
#psy-screen.on { display: flex; }

/* Taller than the viewport: start at the top rather than clipping the head off
   a centred block that cannot be scrolled up to. */
#psy-screen.on > .psy-view { margin: auto; }

/* The response grid is sized to the viewport and must never scroll — a matrix
   that moves under the finger is a mis-tap. Which means everything below it has
   to be *laid out* inside the viewport rather than trusted to fit: with
   `overflow: hidden`, anything that does not fit is not clipped visibly, it is
   simply gone, and the line telling the participant what to do is the last
   thing in the column. So the view becomes a flex column of fixed height, the
   grid is the only part allowed to shrink, and the hint is pinned after it. */
#psy-screen.on:has(.psy-grid-wrap) {
  padding: 12px 8px 52px;
  overflow: hidden;
  touch-action: none;
  align-items: stretch;
}

.psy-view:has(.psy-grid-wrap) {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 0;
}

.psy-view { width: 100%; max-width: 640px; text-align: center; animation: psy-in .18s ease; }
/* When the grid is present, let the view fill the screen width */
.psy-view:has(.psy-grid-wrap) { max-width: 100%; padding: 0 8px; }
@keyframes psy-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

.psy-eyebrow { margin: 0 0 .5rem; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--dim); }
.psy-view h1 { font-size: 1.45rem; font-weight: 600; margin: 0 0 .8rem; letter-spacing: -.01em; }
.psy-view h2, .psy-panel-content h2 { font-size: 1.05rem; font-weight: 600; margin: 1.4rem 0 .4rem; }
.psy-view h3, .psy-panel-content h3 {
  font-size: .74rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em;
  color: var(--dim); margin: 1.5rem 0 .3rem;
}
.psy-body, .psy-view p { color: var(--dim); margin: .65rem 0; }
.psy-body strong, .psy-view p strong { color: var(--fg); font-weight: 600; }
.psy-body ol, .psy-body ul { text-align: left; padding-left: 1.25rem; }
.psy-body li { margin: .45rem 0; }
.psy-meta { font-size: .78rem; color: var(--dim); margin-top: .9rem; }
.psy-err { color: var(--bad); }
.psy-ok { color: var(--ok); font-weight: 700; }
.psy-warn { color: var(--warn); }
.psy-ok-line { color: var(--ok); }
.mono { font-family: var(--mono); font-size: .85em; }

/* A response prompt is read under time pressure, from a distance, by someone
   who has just been staring at a fixation cross. It gets its own size. */
.psy-view h1.psy-lg { font-size: clamp(1.9rem, 5vw, 2.9rem); font-weight: 600; margin-bottom: 1.2rem; }

/* Feedback between trials, likewise: glanceable, not read. */
.psy-message { font-size: clamp(1.8rem, 4.5vw, 2.6rem); line-height: 1.25; }
.psy-message .psy-meta { font-size: 1rem; margin-top: .8rem; }

code, .psy-pre { font-family: var(--mono); background: var(--panel); border-radius: 6px; color: #303845; }
code { font-size: .85em; padding: .15em .4em; }
.psy-pre { display: block; padding: .8rem; text-align: left; overflow-x: auto; font-size: .8rem; border: 1px solid var(--line); }

/* ---------- buttons and inputs ---------- */
.psy-btn {
  font: inherit; font-size: 1.15rem; font-weight: 600; color: #fff; cursor: pointer;
  border: 1px solid var(--orange); background: var(--orange);
  border-radius: 10px; padding: .75rem 1.3rem;
  transition: background .12s, border-color .12s, transform .06s;
}
.psy-btn:hover { background: var(--orange-dark); border-color: var(--orange-dark); }
.psy-btn:active { transform: scale(.985); }
.psy-btn:disabled { opacity: .4; cursor: not-allowed; }
/* A pressed button that is still waiting on what comes next: keeps its label,
   gains a spinner, and is visibly not for pressing again. */
.psy-btn.busy:disabled { opacity: .75; cursor: progress; }
.psy-btn-spin { display: inline-block; margin-left: .55em; vertical-align: -0.15em; width: .9em; height: .9em; }
.psy-btn .psy-btn-spin { border-color: rgba(255,255,255,.35); border-top-color: #fff; }
/* Live preview under the calibration slider, drawn by the experiment. */
.psy-calib-preview { margin-top: 1.25rem; }
.psy-btn.primary { background: var(--orange); border-color: var(--orange); color: #fff; font-weight: 700; }
.psy-btn.primary:hover { background: var(--orange-dark); border-color: var(--orange-dark); }
.psy-btn.small { padding: .4rem .8rem; font-size: .9rem; }

/* Outlined: white fill, black rule, black text. Used where the button is a
   label for a key rather than something to press — a filled blue button
   invites a tap, and taps are exactly what we are trying to avoid mid-task. */
.psy-btn.outline {
  background: #fff;
  border: 2px solid #000;
  color: #000;
  font-weight: 600;
  cursor: default;
  padding: .6rem 1.4rem;
}
.psy-btn.outline:hover { background: #fff; border-color: #000; }

/* Key legend for a two-way self-paced branch. */
.psy-keychoice {
  display: flex; flex-direction: column; gap: .5rem;
  align-items: center; margin-top: 1.6rem;
}
.psy-keyrow { display: flex; align-items: center; gap: .7rem; font-size: .95rem; color: var(--dim); }
.psy-keycap {
  min-width: 5.5rem; text-align: center;
  border: 2px solid #000; border-radius: 8px; color: #000;
  padding: .3rem .8rem; font-size: .8rem; letter-spacing: .1em; text-transform: uppercase;
}
.psy-actions { display: flex; gap: .6rem; justify-content: center; flex-wrap: wrap; margin-top: 1.4rem; }

.psy-label { display: block; color: var(--dim); font-size: .82rem; margin: 1.1rem 0 .35rem; text-align: left; }
.psy-input {
  font: inherit; color: inherit; background: #fff;
  border: 1px solid var(--line); border-radius: 10px;
  padding: .6rem .8rem; width: 100%; max-width: 280px; text-align: center;
}
.psy-input.wide { max-width: 100%; text-align: left; font-family: var(--mono); font-size: .82rem; }
.psy-input:focus { outline: none; border-color: var(--accent); }

/* ---------- stimulus stage ---------- */
#psy-stage {
  position: fixed; inset: 0; z-index: 40;
  display: none; align-items: center; justify-content: center;
  background: var(--stage-bg);
}
#psy-stage.on { display: flex; }
#psy-stim {
  position: relative; width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  color: var(--stage-fg); user-select: none;
}

/* "Press the spacebar when you are ready" — shown on the stimulus surface, so
   the participant is already looking at the right place and the right
   luminance when the first trial begins. */
.psy-ready {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1.4rem; text-align: center; color: #000;
}
.psy-ready-main { font-size: clamp(1.6rem, 4vw, 2.4rem); font-weight: 500; }
.psy-ready-sub { font-size: 1rem; color: #666; }
.psy-ready-key {
  display: inline-block; border: 2px solid #000; border-radius: 8px;
  padding: .45rem 3.5rem; font-size: 1rem; letter-spacing: .18em;
}

/* The tap target sits where the fixation cross is about to appear, so the
   finger lands on the fixation point and the gaze is already there. */
.psy-ready-dot {
  width: 4.4rem; height: 4.4rem; border-radius: 50%;
  background: #fff; border: 3px solid #000; cursor: pointer;
  transition: transform .08s ease-out, background .1s;
}
.psy-ready-dot::after {
  content: ''; display: block; width: .5rem; height: .5rem; border-radius: 50%;
  background: #000; margin: auto; position: relative; top: 50%; transform: translateY(-50%);
}
.psy-ready-dot:active { transform: scale(.92); background: #eef3fd; }

/* ---------- corner controls ---------- */
#psy-corners { position: fixed; inset: 0; pointer-events: none; z-index: 50; }
#psy-corners.hidden { display: none; }
.psy-corner { position: absolute; display: flex; gap: .35rem; align-items: center; pointer-events: auto; flex-wrap: wrap; }
.psy-corner.bl { bottom: 10px; left: 12px; }
.psy-corner.br { bottom: 10px; right: 12px; justify-content: flex-end; }

.psy-label-chip { display: flex; flex-direction: column; line-height: 1.2; margin-right: .3rem; }
.psy-title { font-size: .74rem; font-weight: 600; color: var(--fg); }
.psy-sub { font-size: .62rem; color: var(--dim); letter-spacing: .05em; text-transform: uppercase; }

.psy-chip-sep { width: 1px; height: .8em; background: var(--line); }

.psy-chip {
  font: inherit; font-size: .72rem; color: var(--dim); cursor: pointer;
  display: inline-flex; align-items: center; gap: .35rem;
  background: rgba(255, 255, 255, .9); border: 1px solid var(--line);
  border-radius: 999px; padding: .28rem .7rem;
  backdrop-filter: blur(8px);
  transition: color .12s, border-color .12s, background .12s;
}
.psy-chip:hover { color: var(--fg); border-color: #c9cfd8; background: #fff; }
.psy-chip.static { cursor: default; }

/* Icon-only: pressed once at the start of a session and never again. */
.psy-chip.icon { padding: .28rem .5rem; font-size: .85rem; line-height: 1; }
.psy-chip.icon.on { color: var(--fg); border-color: #c9cfd8; }

.psy-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dim); flex: none; }
.psy-dot.ok { background: var(--ok); }
.psy-dot.warn { background: #e0a020; }
.psy-dot.err { background: var(--bad); }
.psy-dot.busy { background: var(--accent); animation: psy-pulse 1s infinite; }
@keyframes psy-pulse { 50% { opacity: .3; } }

/* ---------- progress ---------- */
#psy-progress {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 12px;
  width: min(360px, 46vw); display: none; align-items: center; gap: .6rem; z-index: 45;
}
#psy-progress.on { display: flex; }
.psy-track { flex: 1; height: 7px; background: var(--line); border-radius: 4px; overflow: hidden; }
.psy-track > i { display: block; height: 100%; width: 0; background: var(--orange); transition: width .25s; }
#psy-progress span { font-size: .7rem; color: var(--dim); white-space: nowrap; }

/* ---------- responses ---------- */
.psy-choices { display: grid; gap: 10px; margin: 1.3rem 0 .4rem; }
.psy-choice {
  font: inherit; color: inherit; cursor: pointer;
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
  padding: 1rem .3rem; transition: background .12s, border-color .12s, transform .06s;
}
.psy-choice:hover { background: var(--panel); border-color: #c9cfd8; }
.psy-choice:active { transform: scale(.98); }
.psy-choice:disabled { opacity: .45; }
.psy-choice-label { display: block; font-size: 1.7rem; line-height: 1.1; }
.psy-key { display: block; font-size: .66rem; color: var(--dim); margin-top: .45rem; }

/* ---------- the response grid ----------
   Every answer visible at once, each one an unmistakable button: raised, with
   its own border and a press that can be felt through the eyes. On a tablet
   this is the only thing the participant ever touches, so a tap that does not
   visibly land is a tap they will make twice.

   The grid spans most of the screen width. Cells are wider than they are tall
   because the constraint is different in each direction: six columns across a
   whole screen leaves plenty of width, while seven rows plus a prompt have to
   fit the height. Bigger targets are quicker to hit and quicker to find, and a
   26-item search is the slowest part of a trial. */
.psy-grid-wrap {
  display: flex; flex-direction: column; align-items: center;
  width: 100%;
  min-height: 0;
  margin: .2rem 0 0;
}

.psy-grid {
  display: grid;
  grid-template-columns: repeat(var(--psy-cols, 6), 1fr);
  gap: .35rem .3rem;
  width: min(96vw, 1400px);
  padding: .45rem .4rem;
  position: relative;
}

.psy-gcell {
  position: relative;
  z-index: 1;
  font: inherit;
  color: #000;
  cursor: pointer;
  width: 100%;
  height: var(--psy-cell-h, 9vh);
  padding: 0;
  font-size: calc(var(--psy-cell-h, 9vh) * .58);
  line-height: 1.1;
  background: #fff;
  border: 2px solid #d3d8e0;
  border-bottom-width: 4px;          /* the shadow line that makes it read as raised */
  border-radius: 12px;
  box-shadow: 0 1px 0 rgba(20, 24, 29, .04);
  transition: transform .07s ease-out, background .1s, border-color .1s, box-shadow .1s;
}

.psy-gcell:hover:not(:disabled) { border-color: #b6bdc8; }

/* The press: the button travels down onto the page and loses its lower edge,
   so the movement is a real displacement rather than a colour change. */
.psy-gcell:active:not(:disabled) {
  transform: translateY(2px) scale(.97);
  border-bottom-width: 2px;
  background: #eef3fd;
  border-color: var(--accent);
  box-shadow: none;
}

/* Selected, but not yet committed. Has to be unmistakable across the room and
   from the corner of the eye, because the participant is about to look away
   from it to find the spacebar. */
.psy-gcell.chosen {
  background: #dce8fb;
  border-color: var(--accent);
  border-bottom-width: 4px;
  box-shadow: 0 0 0 3px rgba(26, 95, 208, .28);
  transform: none;
}

.psy-gcell:disabled { cursor: default; }
.psy-gcell:disabled:not(.chosen) { opacity: .35; }

/* A combination the orthography does not license is a hole, not a dimmed
   button — it was never an option, and should not look like one that was
   passed over. */
.psy-ghole { height: var(--psy-cell-h, 9vh); z-index: 1; }

/* The standing instruction, and the only thing on the screen that changes
   state.

   It sits well clear of the grid: close to the buttons it reads as a caption
   for the letter that was just tapped, and it is not — it is the next thing to
   do. It stays quiet grey while the participant is still choosing, then, after
   a few seconds with no input, lights up. Someone who has stopped has either
   finished or got stuck, and both want the same answer.

   Yellow rather than a colour change alone: the participant is looking at the
   grid, not at the line under it, so the cue has to be visible in peripheral
   vision. A block of colour is; darker text is not. */
.psy-ghint {
  flex: none;                 /* never squeezed out by the grid above it */
  margin: 2rem auto 0;
  padding: .5rem 1.1rem;
  border-radius: 10px;
  font-size: clamp(1.25rem, 3vh, 1.85rem);
  color: var(--dim);
  background: transparent;
  border: 2px solid transparent;
  transition: color .25s ease, background .25s ease, border-color .25s ease;
}
.psy-ghint.on {
  color: #3d2f00;
  font-weight: 600;
  background: #ffe680;
  border-color: #e8c53c;
}

/* The response screen keeps its prompt small — the grid is the object. */
/* The prompt is read under time pressure by someone who has just been staring
   at a fixation cross, so it gets the size that implies. */
.psy-view:has(.psy-grid-wrap) h1 { margin-bottom: .6rem; font-size: clamp(1.6rem, 4.4vh, 2.6rem); }

@media (max-width: 700px) {
  .psy-grid { gap: .25rem .2rem; }
}

/* ---------- running score ---------- */
.psy-score {
  display: inline-flex; align-items: baseline; gap: .4rem;
  border: 1px solid var(--line); border-radius: 999px;
  padding: .25rem .85rem; font-size: .8rem; color: var(--dim);
}
.psy-score b { font-size: 1rem; color: var(--fg); font-variant-numeric: tabular-nums; }

.psy-earned { font-size: clamp(2.6rem, 8vw, 4.4rem); font-weight: 700; line-height: 1; }
.psy-earned.zero { color: var(--dim); font-weight: 500; }

.psy-slots { display: flex; gap: .7rem; justify-content: center; margin: 1.4rem 0 .6rem; }
.psy-slot {
  width: 3.6rem; height: 4.4rem; display: flex; align-items: center; justify-content: center;
  font-size: 2.3rem; line-height: 1; color: #000;
  background: #fff; border: 2px solid var(--line); border-radius: 12px;
  cursor: pointer; transition: border-color .12s, background .12s;
}
.psy-slot.active { border-color: var(--accent); background: #f2f6fe; }
.psy-slot.empty::after { content: '·'; color: #c9cfd8; }
.psy-slot.empty.active::after { content: ''; }

.psy-palette { display: flex; gap: .4rem; justify-content: center; flex-wrap: wrap; margin: .5rem 0 0; }
.psy-pal {
  font: inherit; color: inherit; cursor: pointer; font-size: 1.15rem;
  background: #fff; border: 1px solid var(--line); border-radius: 8px;
  padding: .4rem .8rem; min-width: 3rem;
}
.psy-pal:hover { border-color: var(--accent); }

/* ---------- calibration ---------- */
.psy-calib { margin-top: .6rem; }
.psy-card {
  height: 54px; margin: 1.1rem auto .8rem;
  border: 2px solid var(--accent); border-radius: 10px;
  background: rgba(26, 95, 208, .07);
}
.psy-slider { width: 100%; max-width: 520px; accent-color: var(--accent); }
.psy-presets { display: flex; gap: .4rem; justify-content: center; flex-wrap: wrap; margin: .8rem 0; }
.psy-readout { font-size: .85rem; color: var(--dim); margin-top: 1rem; }
.psy-readout strong { color: var(--fg); }

/* ---------- overlay panel ---------- */
#psy-panel {
  position: fixed; inset: 0; z-index: 60; display: none;
  background: rgba(20, 24, 29, .28); backdrop-filter: blur(3px);
  padding: 24px; overflow-y: auto;
}
#psy-panel.on { display: block; }
.psy-panel-body {
  max-width: 720px; margin: 0 auto;
  background: #fff; border: 1px solid var(--line); border-radius: 16px;
  box-shadow: 0 12px 40px rgba(20, 24, 29, .14);
  padding: 20px 22px 26px;
}
.psy-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.psy-panel-head h2 { margin: 0; font-size: 1.1rem; }
.psy-panel-content { text-align: left; }
.psy-panel-content .psy-actions { justify-content: flex-start; }

/* ---------- tables ---------- */
.psy-table { width: 100%; border-collapse: collapse; font-size: .82rem; margin: .4rem 0 .8rem; }
.psy-table th, .psy-table td { text-align: left; padding: .38rem .3rem; border-bottom: 1px solid var(--line); }
.psy-table th { color: var(--dim); font-weight: 500; }
.psy-table td { color: var(--dim); }
.psy-table td:first-child { color: var(--fg); }
.psy-table td.n, .psy-table th.n { text-align: right; font-family: var(--mono); color: var(--fg); }

/* ---------- warnings ---------- */
.psy-warnings { list-style: none; padding: 0; margin: .8rem 0; text-align: left; }
.psy-warnings li {
  padding: .65rem .85rem; margin-bottom: .45rem; border-radius: 10px;
  border: 1px solid var(--line); background: var(--panel); font-size: .86rem; color: var(--dim);
}
.psy-warnings li.error { border-color: #f0c4c0; background: #fdf3f2; color: #8c2620; }
.psy-warnings li.warn { border-color: #ecd9a8; background: #fdf9ef; color: #79521a; }

/* ---------- upload state ---------- */
.psy-upload {
  margin-top: 1.2rem; padding: .8rem 1rem;
  border: 1px solid var(--line); border-radius: 10px; background: var(--panel); font-size: .9rem;
}
.psy-spin {
  display: inline-block; width: .8em; height: .8em; vertical-align: -.05em;
  border: 2px solid var(--line); border-top-color: var(--accent);
  border-radius: 50%; animation: psy-spin .7s linear infinite;
}
@keyframes psy-spin { to { transform: rotate(360deg); } }

/* ---------- home page ---------- */
.psy-cards { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); margin: 1.2rem 0; }
.psy-card-link {
  display: block; text-align: left; text-decoration: none; color: inherit;
  background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1rem 1.1rem;
  transition: border-color .12s, box-shadow .12s, transform .06s;
}
.psy-card-link:hover { border-color: var(--accent); box-shadow: 0 4px 16px rgba(20, 24, 29, .07); }
.psy-card-link:active { transform: scale(.99); }
.psy-card-link h3 { margin: 0 0 .3rem; font-size: 1rem; text-transform: none; letter-spacing: 0; color: var(--fg); }
.psy-card-link p { margin: 0; font-size: .82rem; color: var(--dim); }

@media (max-width: 560px) {
  .psy-view h1 { font-size: 1.25rem; }
  .psy-corner.tl .psy-sub { display: none; }
  .psy-slot { width: 3rem; height: 3.8rem; font-size: 2rem; }
}
