/* Asymmetrical project grid — flat colour blocks under loose ink ruling.
   Sizes come from a manual weight (data-w 1..4); the palette is the park palette
   pushed brighter, since these are meant to read as painted blocks, not washes. */
:root{
  --g-coral:#EE9179; --g-ochre:#E3A32E; --g-vermillion:#C24125;
  --g-teal:#47738F;  --g-navy:#26445C;  --g-moss:#3E6E5D;
  --g-terra:#DE7F4E; --g-blush:#F2B9A4;
}
[data-theme="dark"]{
  /* deepened until the light label clears 4.5:1 — see the WCAG pass.
     ochre 2.66, coral 3.25, terra 3.49 and blush 2.42 all failed before. */
  --g-coral:#844A3A; --g-ochre:#73541E; --g-vermillion:#8F3220;
  --g-teal:#365A72;  --g-navy:#1A2F40;  --g-moss:#2C5145;
  --g-terra:#814B2E; --g-blush:#715248;
}

.idxhead{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap;
  margin:clamp(30px,4vw,54px) 0 18px;padding-top:14px;
  position:relative;z-index:20}   /* must outrank .pwrap, or tooltips hide behind the grid */
.idxhead .sec{margin:0;padding:0;order:2}
.idxhead .filters{order:1}

/* ---- the grid ---- */
.pwrap{position:relative}
/* --row is set from JS to the measured column width, so span-n-cols by span-n-rows
   comes out square whatever the container is */
.pgrid{position:relative;display:grid;grid-template-columns:repeat(12,1fr);
  grid-auto-rows:var(--row,90px);grid-auto-flow:row;gap:6px}
.pcanvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:5}

.tile{position:relative;z-index:1;display:flex;flex-direction:column;justify-content:flex-end;
  padding:10px 11px;text-decoration:none;color:var(--t-ink,#15150F);overflow:hidden;
  background:none;outline-offset:3px;transition:color .2s}
.tile[data-w="1"]{grid-column:span 2;grid-row:span 2}
.tile[data-w="2"]{grid-column:span 3;grid-row:span 3}
.tile[data-w="3"]{grid-column:span 4;grid-row:span 4}
.tile[data-w="4"]{grid-column:span 5;grid-row:span 5}
@media(max-width:900px){
  .pgrid{grid-template-columns:repeat(6,1fr)}
  .tile[data-w="1"]{grid-column:span 2;grid-row:span 2}
  .tile[data-w="2"]{grid-column:span 3;grid-row:span 3}
  .tile[data-w="3"]{grid-column:span 3;grid-row:span 4}
  .tile[data-w="4"]{grid-column:span 6;grid-row:span 5}
}
@media(max-width:560px){ .pgrid{grid-template-columns:repeat(4,1fr)}
  .tile[data-w="1"],.tile[data-w="2"]{grid-column:span 2;grid-row:span 3}
  .tile[data-w="3"],.tile[data-w="4"]{grid-column:span 4;grid-row:span 5} }

/* the wobbly outline, drawn per tile */
.tile .fr{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
/* the fill and the outline are the same wobbly path, so the block has a drawn
   edge rather than a crisp CSS rectangle with a line sitting on top of it */
.tile .fr .fill{fill:var(--t-bg);stroke:none}
.tile .fr .edge{fill:none;stroke:currentColor;stroke-width:1.05;opacity:.5;
  stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}

.tile .tmeta{position:relative;z-index:3}
.tile .tn{display:block;font-weight:600;letter-spacing:-.01em;line-height:1.15;
  font-size:13px;text-wrap:balance}
.tile[data-w="3"] .tn,.tile[data-w="4"] .tn{font-size:16px}
.tile[data-w="1"] .tn{font-size:11.5px}
/* The symbol in the corner already says which category this is, so the word is
   redundant on screen — but it stays in the DOM for anything reading the page,
   since the symbol itself is aria-hidden. */
.tile .tk{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap}
.tile .tico{position:absolute;top:10px;left:11px;z-index:3;opacity:.55}
.tile .tico .dot{fill:currentColor}
.tile:hover,.tile:focus-visible{color:var(--t-ink,#15150F)}
/* the hovered tile is painted entirely by the shader — edge included — so its own
   drawing steps aside to avoid two slightly different wobbles fighting */
.tile.hot{background:none;z-index:6}
/* Only the fill gives way — the outline stays put, so hovering cannot change the
   shape of the box. The canvas is clipped to this same outline.

   Gated on .crumble-ok, which crumble.js only sets once WebGL and clip-path are
   both working, and removes if the context is lost. Without that gate any canvas
   failure left a blank square, since the tile had stopped painting itself. */
.pwrap.crumble-ok .tile.hot .fr .fill{opacity:0}
/* filtering drains the non-matching blocks instead of pulling them out — the
   weave has to stay continuous for the grid to keep reading as one fabric */
.tile.dim{opacity:.30;filter:saturate(.25)}
.tile.dim .tn,.tile.dim .tk,.tile.dim .tico{opacity:.5}
.tile,.mlabel{transition:opacity .25s,filter .25s}
.mlabel.dim{opacity:.22}
.tile[hidden]{display:none}

/* the ruling that overshoots the blocks, as in the reference sheets */
.prules{position:absolute;inset:-30px -20px;width:calc(100% + 40px);height:calc(100% + 60px);
  pointer-events:none;z-index:4;overflow:visible}
.prules path{fill:none;stroke:var(--ink);stroke-width:.9;opacity:.42;
  stroke-linecap:round;vector-effect:non-scaling-stroke}
.prules path.faint{opacity:.22}

/* filter symbols — same marks as the tiles and the map */
.filters{display:flex;flex-wrap:wrap;gap:11px;align-items:center}
.filters button{position:relative;font:inherit;padding:0;cursor:pointer;background:none;
  border:0;line-height:0;display:block;opacity:1;transition:opacity .18s,transform .18s}
.filters button[data-f="app"]{color:var(--g-teal)}
.filters button[data-f="game"]{color:var(--g-moss)}
.filters button[data-f="event"]{color:var(--g-vermillion)}
.filters button[data-f="exploration"]{color:var(--g-ochre)}
.filters button[data-f="visualization"]{color:var(--g-terra)}
.filters button svg{display:block;overflow:visible;pointer-events:none}
.filters button .dot{fill:currentColor}
.filters button:hover{transform:scale(1.18)}
/* only while filtering do the others step back */
.filters button svg{transition:opacity .18s}
.filters.filtering button[aria-checked="false"] svg{opacity:.28}
.filters.filtering button[aria-checked="true"]{transform:scale(1.18)}
.filters button::after{content:attr(aria-label);position:absolute;left:50%;top:calc(100% + 9px);
  transform:translateX(-50%);white-space:nowrap;font-family:var(--mono,ui-monospace,monospace);
  font-size:8.5px;letter-spacing:.12em;line-height:1.35;color:var(--paper);background:var(--ink);
  padding:6px 10px;border-radius:2px;opacity:0;pointer-events:none;transition:opacity .14s;z-index:9}
.filters button:hover::after,.filters button:focus-visible::after{opacity:1}

/* the shared lattice: the grid of projects and the empty grid above it are the
   same weave, drawn on the same pitch */
.gridsvg path{fill:none;stroke:var(--ink);stroke-width:.9;opacity:.13;
  stroke-linecap:round;vector-effect:non-scaling-stroke}
.gridsvg path.ahead{opacity:.07}     /* cells above the first project: room to come */

/* ---- month markers ----
   Not headings: each month takes a cell in the weave, so its name is read in the
   empty space beside its projects. Proximity does the grouping. */
.mlabel{grid-column:span 2;grid-row:span 1;display:flex;flex-direction:column;
  justify-content:center;gap:3px;padding:0 3px;line-height:1.1;pointer-events:none;
  font-family:var(--mono,ui-monospace,monospace)}
.mlabel span{opacity:.8;font-weight:600}
.mlabel b{font-family:var(--mono,ui-monospace,monospace);font-size:8.5px;
  font-weight:400;letter-spacing:.16em;opacity:.38;color:var(--ink)}
@media(max-width:560px){ .mlabel{grid-column:span 2} }

/* the heading doubles as the way back to everything — a real button, so it is
   reachable by keyboard and announced as an action rather than just a heading */
/* ---- one micro-type scale ----
   The month markers, the ALL PROJECTS heading and the footer are the same voice:
   same family, size and tracking, all coloured from --ink so light and dark are
   handled by the theme rather than by separate rules. Only weight and opacity
   separate them, and only ALL PROJECTS takes a hover state, because it is the
   only one of the three you can act on. */
.mlabel span,
.idxhead .sec,
.foot-meta{font-family:var(--mono,ui-monospace,monospace);font-size:9.5px;
  letter-spacing:.2em;color:var(--ink)}

.idxhead .sec{font-weight:600;margin:0;padding:0;order:2;opacity:.8}
.sec .secbtn{font:inherit;letter-spacing:inherit;color:inherit;background:none;border:0;
  padding:0;margin:0;cursor:pointer;-webkit-appearance:none;appearance:none;
  transition:color .18s}
.idxhead .sec:hover,.idxhead.filtering .sec{opacity:1}
.sec .secbtn:hover{color:var(--g-vermillion)}
.sec .secbtn:focus-visible{outline:1px solid currentColor;outline-offset:4px}

/* the link out sits on a word in the subtitle, so it has to stay quiet enough
   not to break the sentence */
/* the same quiet link treatment wherever it appears — subtitle and footer */
.lede .me,.foot-meta .me{color:inherit;text-decoration:none;
  border-bottom:1px solid var(--g-vermillion);padding-bottom:1px;
  transition:color .18s,opacity .18s}
.foot-meta .me{border-bottom-color:var(--g-vermillion)}
.lede .me:hover,.foot-meta .me:hover{color:var(--g-vermillion)}
.lede .me:focus-visible,.foot-meta .me:focus-visible{outline:2px solid var(--g-vermillion);outline-offset:3px}

/* dark-mode toggle, sitting with the footer meta */
.foot-meta{opacity:1;padding-top:16px}
.foot-meta,.foot-meta a{color:var(--ink)}
.foot-meta > span{opacity:.8}
.foot-meta a{text-decoration:none;border-bottom:1px solid currentColor;padding-bottom:1px;
  transition:color .18s,border-color .18s}
.foot-meta a:hover{color:var(--g-vermillion)}
.foot-left{display:inline-flex;align-items:center;gap:10px}
.foot-left .themebtn{margin-right:2px}
.foot-right{display:inline-flex;align-items:center;gap:8px}
.foot-right i{font-style:normal;opacity:.4}
.foot-right a{opacity:.85}
.foot-right a:hover{opacity:1}

.foot-meta .themebtn{width:13px;height:13px;flex:none;border:1px solid currentColor;
  background:none;cursor:pointer;border-radius:50%;padding:0;position:relative;
  overflow:hidden;color:inherit;opacity:.7;transition:opacity .16s}
.foot-meta .themebtn::after{content:"";position:absolute;inset:0 50% 0 0;background:currentColor}
.foot-meta .themebtn:hover{opacity:1}
.foot-meta .themebtn:focus-visible{outline:2px solid var(--g-vermillion);outline-offset:3px}

/* the map is drawn 30% larger on desktop (see buildViz); the cap has to move
   with it or it would just be scaled back down again */
@media(min-width:900px){
  .viz svg{max-width:820px}
}

/* The title sat low because .hero is align-items:end and the (now larger) map
   sets the row height. Opening space above the links lifts the title and
   subtitle while the links themselves stay where they were — aligning the whole
   block to the top would have moved them too. */
@media(min-width:940px){
  /* a healthy gap under the title, and a bigger one above the links so the
     title/subtitle pair sits well clear of them.
     Written as p.lede because park.css sets p.lede{margin-top:14px} — a bare
     .lede loses on specificity however late it is declared. */
  p.lede{margin-top:24px;font-size:15px}
  .herolinks{margin-top:64px;margin-bottom:12px;font-size:15px}
}

/* ---- dark mode: the category symbols come on like lamps ----
   Same --lamp the park's bulbs use, so the page reads as one lit scene after
   dark. Two drop-shadows give a tight core and a soft halo; the slow breath is
   staggered per tile (--gd) so they do not pulse in lockstep. */
[data-theme="dark"] .tile .tico{
  opacity:1;
  color:var(--lamp);
  filter:drop-shadow(0 0 2px rgba(255,179,60,.95))
         drop-shadow(0 0 7px rgba(255,179,60,.55))
         drop-shadow(0 0 15px rgba(255,179,60,.25));
  animation:tileglow 4.6s ease-in-out infinite;
  animation-delay:var(--gd,0s);
}
[data-theme="dark"] .tile .tico .dot{fill:var(--lamp)}

/* Every block is dark after nightfall, so all labels need the light foreground.
   That is --ink here, NOT --paper: the two swap roles between themes (dark
   --paper is #0E1218, near-black, which is what made navy tiles unreadable).
   The per-tile --t-ink is inline for the light palette and cannot be reassigned
   from a stylesheet, so the resolved colour is overridden instead. */
[data-theme="dark"] .tile{color:var(--tile-label)}
/* full strength: .96 was enough to pull terra from 4.60 to 4.38:1 */
[data-theme="dark"] .tile .tn{opacity:1}
@keyframes tileglow{
  0%,100%{opacity:.82}
  50%    {opacity:1}
}
/* a dimmed tile is out of the current filter, so its lamp is off too */
[data-theme="dark"] .tile.dim .tico{filter:none;animation:none;opacity:.35}
@media (prefers-reduced-motion: reduce){
  [data-theme="dark"] .tile .tico{animation:none}
}

/* ---- subtitle contrast ----
   park.css sets p.lede{opacity:.75} and playground.css colours it with --dim,
   which is already 65% alpha. The two compound to 0.4875, giving 3.26:1 in light
   and 3.73:1 in dark — both under the 4.5:1 WCAG AA needs for 13.5px/400 text.
   The element opacity goes, so --dim alone governs, and --dim is raised to meet
   AA in both themes with a little headroom. */
p.lede{opacity:1}
.lede{color:var(--lede-fg)}
:root{--lede-fg:#141414c9}
[data-theme="dark"]{--lede-fg:#C9D2DAd6}

/* ---- the project map reads, it does not navigate ----
   Nothing in the map is clickable — hovering a cell only writes to the readout —
   so a pointer/hand would promise a click that never comes. Crosshair is the
   convention for precise inspection on a chart, and it is what grid-viz.html
   used before that file was folded away. Set on the svg rather than the hit
   rects so the cursor does not flicker in the gaps between cells. */
.viz svg{cursor:crosshair}
.viz .hit{cursor:crosshair}
/* the readout is the feedback the cursor is promising, so give it presence */
.viz-read{opacity:0;transition:opacity .15s}
.viz:hover .viz-read{opacity:1}

/* ---- small type that fell under 4.5:1 in the WCAG pass ---- */
/* the external-link arrow: 10px/500 at .55 alpha read 3.94:1 */
.herolinks .ext{opacity:.85}
/* the year beside each month: 8.5px/400 at .38 alpha read 2.40:1 */
.mlabel b{opacity:.72}

/* ---- work in progress ----
   A tile with no destination yet still looks and behaves like a link, which is a
   promise it cannot keep. It gets a tag, loses the pointer, and its click is
   cancelled in grid-new.js. currentColor is the tile's own label colour, which
   already clears 4.5:1 against its tint, so the tag inherits that contrast. */
.tile[href="#"]{cursor:default}
.tile[href="#"]::after,
.tile[data-wip]::after{
  content:"WORK IN PROGRESS";
  position:absolute;top:10px;right:11px;z-index:4;
  /* inline-flex + place-items centres the text in the pill; the extra left pad
     offsets the trailing letter-space, which otherwise pushes it off-centre */
  display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--mono,ui-monospace,monospace);font-size:7.5px;letter-spacing:.16em;
  line-height:1;padding:4px 5px 4px 6px;
  /* --tile-label, not --paper: the paper token is near-black in dark mode,
     which is what made this tag unreadable there */
  color:var(--tile-label);border:1px solid currentColor;border-radius:2px;
  background:rgba(18,22,27,.30);
  opacity:.92;pointer-events:none}
.tile[href="#"]:hover::after,.tile[data-wip]:hover::after{opacity:1}
@media(max-width:560px){
  .tile[data-w="1"][href="#"]::after,.tile[data-w="1"][data-wip]::after{content:"WIP"} }


/* ---- labels ----
   One fixed off-white in both themes. NOT var(--paper): that token swaps meaning
   between light and dark, which is what turned the labels near-black in dark
   mode. The label always sits on a coloured block, so it always wants the light
   value regardless of theme. */
:root{--tile-label:#F4F1EC}
[data-theme="dark"]{--tile-label:#E9EEF3}
.tile{color:var(--tile-label)}
.tile .tico{color:var(--tile-label)}

/* ---- tile outlines after dark ----
   The label colour drives the outline, which after dark is a light off-white —
   it read as a bright rim against the deep background. Dark mode uses the same
   stroke and weight as the lattice behind the grid instead, so the outline sits
   with the weave rather than in front of it. */
[data-theme="dark"] .tile .fr .edge{stroke:var(--ink);opacity:.15}
[data-theme="dark"] .tile.hot .fr .edge{opacity:.28}
