/* =============================================================================
   board.css — the landing board: graph paper, three tiers of tile, captions.

   1  Board + paper     soft grid, the board's frame, index labels, confetti
   2  Tiles             position is data: --x --y --w --h written by board.js
                        every frame; one frame for every tile, and what
                        still tells the tiers apart; the press
   3  Media             project picture + number (and its kin ink), logo,
                        blog text, post slots and their crossfade
   4  Decoration        faces: colour, pattern, type; the idle swap's fade
   5  Caption           the strip a tile uncovers as it grows
   6  ?grid debug overlay
   7  Grid mode         .is-grid — the narrow-screen structure (mobile.css
                        styles it)

   The board is one object: a framed sheet of soft graph paper with tiles
   laid on its cells. At rest every cell is `--cell`, a whole px. A hover
   stretches the sheet: board.js springs every column and row track and
   writes the grid lines (--gx0 … --gx10, --gy0 … --gy10) on .board and each
   tile's four numbers on the tile, every frame. Nothing in this file
   transitions a position or a size — the paper's lines, its index labels,
   the confetti and the tiles are all placed off those same lines, so they
   cannot drift apart (board.js header).

   Every tile wears the same frame: one ink hairline. A board where some
   things had an edge and some did not read as two systems laid over each
   other (a colour block floating unframed beside a framed print). The tiers
   are told apart by what is inside the line, never by the line itself:
   - Framed work (projects): a paper mat inside the frame, then the picture
     — a print hung on the wall.
   - Marks (logos, the blog, posts): a lighter card, the line on its edge.
   - Decoration (colour, pattern, type): the swatch or drawing runs to the
     line — printed matter, no card.
   Confetti is not a tile (a chip printed on the paper) and has no frame.
   The board's own edge stays the softer --line-strong: the sheet, not a
   thing on it. No radius, no shadows. The one px values are the 3px mat
   and the hairline.

   Caption type is fitted to the cell between a floor and the small token:
   clamp(--cap-floor, 17% of a cell, --fs-small). The floor is the small
   token × 0.94 (≈ 11px at 1280, ≈ 12.4px at 1440) — below that, captions
   stop being read and start being deciphered, so a caption that does not
   fit at the floor is clamped and board.js warns, rather than shrunk.
============================================================================= */


/* -- 1  Board + paper -------------------------------------------------------- */

.board {
  /* The caption arrives a beat into the spring (it has room to be read by
     then) and leaves at once, so a shrinking tile never shows words being
     squeezed out of it. Plain ms, not tokens: these are not page motion. */
  --cap-in: 160ms;
  --cap-in-delay: 120ms;
  --cap-out: 60ms;   /* gone before the strip has visibly closed */
  --cap-floor: calc(var(--fs-small) * 0.94);
  --cap-fs: clamp(var(--cap-floor), calc(var(--cell) * 0.17), var(--fs-small));
  --mat: 3px;           /* the paper gap inside a frame                */
  --t-swap: 400ms;      /* the idle swap's crossfade (board.js SWAP_FADE) */
  --t-slot: 300ms;      /* a post slot taking its fate (board.js SLOT_FADE) */
  position: relative;
  /* board.js writes width / height / --cell in whole px. */
}

/*
   The grid is the sheet the composition is laid on, not a table it is filed
   in: hairlines in the soft line, one element per line, each placed on a
   track edge (board.js §4), so the lines stretch with the tracks and a tile
   is always sitting on the grid, never sliding over it. The sheet's own
   edge is the one strong rule (::after), so the whole board reads as one
   object resting on the page. (Grid mode, which has no tracks, draws the
   same lines as a repeating gradient — §7.)
*/
.board-paper {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.board-lines > span {
  position: absolute;
  background: var(--line-soft);
}
.board-lines > .is-v { top: 0; bottom: 0; width: var(--hair); }
.board-lines > .is-h { left: 0; right: 0; height: var(--hair); }

.board-paper::after {
  content: '';
  position: absolute;
  inset: 0;
  border: var(--hair) solid var(--line-strong);
}

/*
   Every empty cell carries its index, like a contact sheet's frame numbers:
   so small and so pale it is texture first and a number only up close. It
   is derived from the small token (×0.7), not a fourth size. Tiles sit
   above this layer, so a growing tile covers the labels it grows over; the
   rest ride their cell's corner (left / top are inline, off the track
   edges).

   Only cells nothing rests on carry one (board.js), and each label is the
   size of its own cell (width / height inline, off the same edges) and
   clipped to it: squeezed under 0.55 of a cell by a neighbour's hover, a
   label is cut by its own cell's edge rather than reaching past it and
   peeking out from under a tile that is moving.
*/
.board-index > span {
  position: absolute;
  overflow: hidden;
  padding: 0.35rem;
  font-size: calc(var(--fs-small) * 0.7);
  line-height: 1;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: color-mix(in srgb, var(--ink) 20%, transparent);
}

/*
   Confetti: a solid chip in one quadrant of an empty cell, printed on the
   paper. A full half cell read as a fifth kind of tile; at 0.36 of a cell,
   pushed into its quadrant's outer corner, it reads as what it is — a
   scatter of colour between the things that matter. Its cell's four edges
   (--l --r --t --b) are inline, off the track edges, and everything else
   is a fraction of that cell: when the cell stretches, the chip keeps its
   corner and its proportion.
*/
.board-bit {
  --bit: 0.36;
  --qx: 0;
  --qy: 0;
  position: absolute;
  left: calc(var(--l) + (var(--r) - var(--l)) * var(--qx));
  top: calc(var(--t) + (var(--b) - var(--t)) * var(--qy));
  width: calc((var(--r) - var(--l)) * var(--bit));
  height: calc((var(--b) - var(--t)) * var(--bit));
}
.board-bit.q-tr,
.board-bit.q-br { --qx: calc(1 - var(--bit)); }
.board-bit.q-bl,
.board-bit.q-br { --qy: calc(1 - var(--bit)); }


/* -- 2  Tiles ---------------------------------------------------------------- */

.tile {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  display: block;
  width: var(--w);
  height: var(--h);
  translate: var(--x) var(--y);
  color: inherit;
  -webkit-tap-highlight-color: transparent;
  /* No transition, ever: board.js moves these every frame on its spring,
     and a transition on top would lag every frame behind it. */
}

/* Tiles never overlap, so this only lifts the hot tile's focus ring over
   its neighbours. */
.tile.is-hot { z-index: 2; }

/*
   Press, then lift. A held link answers before it navigates: a print sinks
   into its mat, a card of words dims. Only the picture moves — never the
   frame, the mat or the number chip — so the tile keeps its place on the
   sheet; the flyer reads the picture's rect at the click, so a flight
   starts from wherever the press has it. In quickly (a third of a hover),
   out on the hover's own pace. Outside the hover gate on purpose: a tap
   is a press too. Only [href]: Chrome gives :active to any element under
   a held button, so a logo card or a waiting slot would sink for nothing.
   Opacity on the cards, not scale: their words already scale by --sq.
*/
.tile--project .tile-media > img,
.tile--project .tile-media > .tile-fill { transition: scale var(--t-hover) var(--ease-enter); }
.tile--project[href]:active .tile-media > img,
.tile--project[href]:active .tile-media > .tile-fill {
  scale: 0.97;
  transition-duration: calc(var(--t-hover) * 0.3);
}
.tile--blog .tile-media,
.tile--post .tile-media { transition: opacity var(--t-hover) var(--ease-enter); }
.tile--blog[href]:active .tile-media,
.tile--post[href]:active .tile-media {
  opacity: 0.6;
  transition-duration: calc(var(--t-hover) * 0.3);
}
/* Nothing to follow: a logo card or the fallback blog tile is read, not
   clicked. */
.tile:not([href]) { cursor: default; }

/* The entrance ([data-seq]) lives on this inner layer, so it never fights
   the outer layer's translate.

   It reaches one hairline past the tile's right and bottom edges. The
   paper draws the grid line at x as the hairline [x, x + hair] — so a
   frame drawn inside its own box would put a tile's right line on
   [x − hair, x], beside the neighbour's left line on [x, x + hair], and two
   touching tiles would read as a 2px rule. Reaching one hairline over, a
   tile's right and bottom lines land on the grid line itself, which is
   exactly where the next tile's left and top lines are: touching tiles
   share one hairline, and a tile beside empty paper sits on the paper's
   own line. On the board's right and bottom edges (.at-r / .at-b,
   board.js) there is no line past the edge to reach — the sheet's frame
   is at [W − hair, W] — so those tiles stay inside and close on it. */
.tile-in {
  position: relative;
  display: flex;
  flex-direction: column;
  width: calc(100% + var(--hair));
  height: calc(100% + var(--hair));
  overflow: hidden;
  background: var(--paper-2);
}
.tile.at-r > .tile-in { width: 100%; }
.tile.at-b > .tile-in { height: 100%; }

/*
   The frame: one ink hairline on every tile, drawn over everything inside
   it (the last thing painted in the tile, after the picture, the number
   chip and any swapped face). On the inner layer, so it arrives and leaves
   with what it frames rather than standing empty during the entrance. A
   pseudo-element, not the tile's border (which would push the content in
   on two sides only) nor its outline (the focus ring owns outline on the
   tile, base.css §8, and an inner outline can paint under positioned
   children).
*/
.tile-in::after {
  content: '';
  position: absolute;
  inset: 0;
  border: var(--hair) solid var(--ink);
  pointer-events: none;
}

/*
   Framed work: inside the line a paper mat, then the picture — a print
   hung on the wall. The padding is the line plus the mat, since the line
   is painted over the box rather than taking room in it.
*/
.tile--project > .tile-in {
  padding: calc(var(--hair) + var(--mat));
  background: var(--paper);
}

/* Decoration answers nothing: no pointer, so it never takes a hover or a
   click from the tile beside it. It has no card: the swatch or drawing
   runs to the frame. */
.tile--deco { pointer-events: none; }
.tile--deco > .tile-in { background: none; }



/* -- 3  Media ---------------------------------------------------------------- */

/* Fills whatever the caption strip does not. Both grow with the tile, in
   proportion (board.js capOpen), so the picture swells on both axes as the
   strip opens under it. */
.tile-media {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.tile-media > img,
.tile-fill {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.tile-media > img { object-fit: cover; }
.tile-fill { display: block; }

/*
   The project's number: a small paper chip on the picture's bottom-left
   corner. It used to be type in the picture's own light tone, which held on
   a dark placeholder and vanished on a light export (the remittance thumb is
   a phone on paper). A chip reads on anything, so it is one system for
   photographs and gradients alike: paper fill, the strong hairline every
   resting object has, ink figures. It belongs to the picture, not the tile,
   so it rides up on top of the caption strip as that opens. The size is the
   small token × 0.85 — derived, not a fourth size.
*/
.tile-no {
  position: absolute;
  left: 0.5rem;
  bottom: 0.5rem;
  padding: 0.1em 0.45em;
  border: var(--hair) solid var(--line-strong);
  background: var(--paper);
  font-size: calc(var(--fs-small) * 0.85);
  font-weight: 500;
  line-height: 1.2;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
  pointer-events: none;
  /* The employer lights its work: while a logo is open, the chips of the
     projects done there turn ink (board.js apply, .is-kin) — colour only,
     together, on the hover's pace. */
  transition:
    background-color var(--t-hover) ease-out,
    border-color     var(--t-hover) ease-out,
    color            var(--t-hover) ease-out;
}
.tile.is-kin .tile-no {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper-2);
}

/* Logo: the mark is pinned to the card's top-left corner, a square the
   size of a cell — or of the tile, while a neighbour's hover has it
   squeezed under one cell, so the mark shrinks with it rather than being
   cut. The card grows around it and the words arrive underneath, on the
   same left edge (the mark's inset ≈ the caption's padding). */
.tile--logo .tile-media {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
}

.tile--logo .tile-media > img {
  --m: min(var(--cell), var(--w, var(--cell)), calc(var(--h, var(--cell)) - var(--capv, 0px)));
  position: static;
  flex: none;
  width: var(--m);
  height: var(--m);
  /* The image box is 76% of the cell (0.12 of a cell each side) times the
     logo's own --logo-scale (content.js logos[].scale, written by board.js,
     default 1), which evens out how heavy the marks look: GoPay 1.05 (inset
     ≈ 0.10), Bukalapak 1 (0.12), Kargo 0.95 (≈ 0.14). The files carry their
     own white margin, so the visible mark lands at about half the cell
     (GoPay ≈ 54 × 50%, Kargo 43 × 63%, Bukalapak 54 × 57% of the file) and
     ends by about 0.74 of it. On an open card the caption starts a full
     cell down, under the mark's box. */
  padding: calc(var(--m) * (1 - 0.76 * var(--logo-scale, 1)) / 2);
  object-fit: contain;
  mix-blend-mode: multiply;   /* the marks come on white */
}

/* Blog: words, not a picture, so they are always showing. Same inset as a
   caption, so the resting words and the row that opens under them share
   one left edge; set in the small token, a step back in ink (below). */
.tile-blog {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 0.5rem 0.75rem;
  font-size: var(--fs-small);
  line-height: 1.25;
}
/* On the board (not in grid mode) the words are set once at the resting
   size, top-left, and scaled by the squeeze (--sq, board.js) — so a
   neighbour's hover shrinks them with the tile instead of re-wrapping
   them into a word that is cut off. Open, the tile's extra room is air
   to their right and the caption strip under them. */
.board:not(.is-grid) .tile-blog {
  flex: none;
  width: var(--bw);
  height: var(--bh);
  scale: var(--sq, 1);
  transform-origin: 0 0;
}

/* The text tiles sit one step back from the project captions: the label
   in the quietest ink, the title in ink-2 rather than ink. */
.tile-blog-label { color: var(--ink-3); }
/* Measured to the resting tile, so opening it never re-wraps the words. */
.tile-blog-title {
  max-width: calc(var(--bw, 100%) - 1.5rem);
  font-weight: 500;
  color: var(--ink-2);
  text-wrap: balance;   /* two even lines, never a lone last word */
}

/* "All posts": one cell, so the inset is a step tighter than the 2 × 1
   tile's and the two words may wrap ("All / posts ↗"). */
.tile-all { padding: 0.45rem 0.55rem; }
.tile-all .tile-blog-title { max-width: none; }

/*
   Post slots (board.js §4b). A slot holds one face — the waiting blog
   look, a post, or (failing that) a colour laid over it — and a change of
   fate lays the new face over the old and fades it in over --t-slot, then
   drops the old. The faces are stacked at the slot's top-left, set at the
   resting size and scaled by the squeeze, like the blog tile's words.
*/
.tile-slot > * {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transition: opacity var(--t-slot) ease-in-out;
}
.board:not(.is-grid) .tile-slot > * {
  width: var(--bw);
  height: var(--bh);
  scale: var(--sq, 1);
  transform-origin: 0 0;
}
.tile-slot > .is-new,
.tile--post > .tile-in > .deco-face.is-new { opacity: 0; }
.tile--post > .tile-in > .deco-face { transition-duration: var(--t-slot); }

/* Waiting for the feed: the blog look, its label fainter still — a slot
   that says writing lives here, not a tile asking to be read. */
.tile-blog.is-pending .tile-blog-label { color: color-mix(in srgb, var(--ink-3) 55%, transparent); }
.tile--post.is-pending { cursor: default; }

/* A slot with no post behind it is paper: still laid out, never seen. */
.tile--post.is-empty {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-slot) ease-in-out;
}

/* A post: its title in the small token at 500, up to three lines, and
   the date under it in the quietest ink. Same inset as the blog tile, so
   a row of posts and the All posts tile share one left edge. */
.tile-post {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 0.25rem;
  padding: 0.5rem 0.75rem;
  font-size: var(--fs-small);
  line-height: 1.2;
}
.tile-post-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  font-weight: 500;
  color: var(--ink);
  text-wrap: pretty;
}
.tile-post-date {
  flex: none;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* A face's lines never shrink to fit (a clamped title would give up its
   last line silently and fitFace would think it fitted): they overflow,
   and fitFace sees it. */
.tile-blog > *,
.tile-post > * { flex: none; }

/* A face that does not fit its resting tile (board.js fitFace): tighter
   leading and inset first, then the label goes. */
.tile-blog.is-tight,
.tile-post.is-tight {
  gap: 0.1rem;
  padding-block: 0.35rem;
  line-height: 1.12;
}
.tile-blog.is-bare > .tile-blog-label,
.tile-blog.is-quiet > .tile-blog-title { display: none; }
.tile-blog.is-bare { justify-content: flex-end; }


/* -- 4  Decoration ----------------------------------------------------------- */
/*
   A decorative tile is one or more stacked faces. The idle swap (board.js
   §6) appends a new face with .is-new (transparent), lets it fade in over
   the old one, and then removes the old — so a swap never shows paper
   between two colours.
*/

.deco-face {
  position: absolute;
  inset: 0;
  display: block;
  color: var(--ink);
  transition: opacity var(--t-swap) ease-in-out;
}
.deco-face.is-new { opacity: 0; }
.deco-face.is-dark { color: var(--paper-2); }

/* Pattern: hairlines in ink at 70% on the lighter card, or on a swatch.
   The stroke never scales with the drawing (vector-effect in board.js), so
   every line is the board's own hairline. */
.deco-pattern { background-color: var(--paper-2); }
.deco-pattern > svg {
  width: 100%;
  height: 100%;
  max-width: none;
  fill: none;
  stroke: color-mix(in srgb, currentColor 70%, transparent);
  stroke-width: var(--hair);
}
.deco-pattern .dots {
  fill: color-mix(in srgb, currentColor 70%, transparent);
  stroke: none;
}

/*
   Type: a fragment, set on a swatch or the lighter card. `md` is a word in
   the body token at the foot of the cell. `xl` is a glyph cut by its cell
   like a cropped headline — sized from its own box (a size container, so
   cqh is the tile's height whatever the board makes it, whole cells or
   not), not from a type token, because what it has to do is overfill a
   box whose size the board computes.
*/
.deco-type {
  display: flex;
  overflow: hidden;
  background-color: var(--paper-2);
}

.deco-type--md {
  align-items: flex-end;
  padding: 0.5rem 0.6rem;
  font-size: var(--fs-body);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.01em;
}
/* A word is printed on its tile: squeezed, it shrinks toward its corner
   (--sq, board.js) rather than losing its last letters. */
.deco-type--md > span {
  scale: var(--sq, 1);
  transform-origin: 0 100%;
}

.tile--type > .tile-in { container-type: size; }

.deco-type--xl {
  align-items: flex-end;
  font-size: 132cqh;
  font-weight: 600;
  line-height: 0.72;
  letter-spacing: -0.06em;
}
.deco-type--xl > span {
  margin: 0 0 -0.04em -0.07em;   /* cut by the cell's left and bottom edges */
  white-space: nowrap;
}


/* -- 5  Caption -------------------------------------------------------------- */
/*
   The strip under the picture. Its height (--capv) is written by board.js
   every frame: the caption's own 0…1 spring times its full height, so the
   strip opens with the tile and closes with it. The words inside are laid
   out once, at the open width (--ow), and pinned to the strip's top edge:
   they rise out of the tile's bottom edge as the strip opens and never
   re-wrap or squash. Opacity is the only transition here — in late, out
   at once.

   A caption is never cut. Its full height (--caph) is not a number of
   cells: board.js measures each caption's natural height at its open
   width (sizeBoard) and the hover grows the tile by exactly that much
   (plus the picture's own swell, for framed work) — so a why that wraps
   to three lines at 1280 gets a taller strip, not an ellipsis. No line
   clamp, no text-overflow, anywhere in a caption; checkCaptions asserts
   it at boot.
*/

.tile-cap {
  flex: none;
  height: var(--capv, 0px);
  overflow: hidden;
  opacity: 0;
  /* Out front-loaded, not ease-in: ease-in held the words at half strength
     for the first 40ms, exactly while the strip starts to close over them.
     On this curve they are at ~20% by 20ms and under 5% by 40ms. */
  transition: opacity var(--cap-out) cubic-bezier(0, 0, 0, 1);
}

.tile.is-hot .tile-cap {
  opacity: 1;
  transition: opacity var(--cap-in) ease-out var(--cap-in-delay);
}

/* Type is fitted to the cell between the floor and the small token (see
   the header). 0.75rem in from the tile edge so the words never sit flush
   against the hairline frame. */
.tile-cap-in {
  display: flex;
  flex-direction: column;
  width: var(--ow, 100%);
  height: var(--caph, auto);
  padding: 0.6rem 0.75rem 0.45rem;
  border-top: var(--hair) solid var(--line);
  font-size: var(--cap-fs);
  line-height: 1.25;
  overflow: hidden;
}

/* Inside a frame the strip is the mat's width narrower, and its rule would
   only repeat the mat's edge: the caption reads as the label under the
   print, set on the same paper. */
.tile--project .tile-cap-in {
  width: calc(var(--ow, 100%) - 2 * (var(--mat) + var(--hair)));
  padding: 0.55rem calc(0.75rem - var(--mat)) 0.35rem;
  border-top: 0;
}

.tile-cap-in > * { flex: none; }

/* Never truncated: a title that does not fit wraps, and the strip is
   measured to hold it. */
.tile-title {
  display: block;
  font-weight: 500;
  color: var(--ink);
  text-wrap: balance;
}

/* A project's why, a logo's role, the blog's note: as many lines as they
   take at the open width — the strip is measured to hold them. */
.tile-why,
.tile-role {
  display: block;
  color: var(--ink-2);
  text-wrap: pretty;
}

.tile-years,
.tile-note { color: var(--ink-3); }
.tile-years { font-variant-numeric: tabular-nums; }

/* A logo card is one sheet: mark, then words. No rule between them — the
   caption starts a few px under the mark, where a line would only crowd it.
   The horizontal inset is a touch less than a project caption's so a
   two-line role ("Senior Product Designer, Transfers") still breaks into
   two lines in a 2-cell card at 1280; never seen next to a project
   caption, so the two edges are never compared. */
.tile--logo .tile-cap-in {
  border-top: 0;
  padding-inline: 0.6rem;
}

@media (prefers-reduced-motion: reduce) {
  .board { --cap-in: 1ms; --cap-in-delay: 0ms; --cap-out: 1ms; --t-slot: 1ms; }
}


/* -- 6  ?grid debug overlay -------------------------------------------------- */

.board-debug {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.board-debug > span {
  position: absolute;
  padding: 0.2em 0.35em;
  font-size: min(var(--fs-small), calc(var(--cell) * 0.15));
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  opacity: 0.7;
}

/* Which board this is (board.js drawDebug): mode, seed, score, size,
   fill — on a paper chip above the board's top edge. */
.board-debug > .board-debug-info {
  position: absolute;
  top: auto;
  bottom: 100%;
  left: 0;
  margin: 0 0 0.35rem;
  padding: 0.2em 0.45em;
  background: var(--paper-2);
  border: var(--hair) solid var(--line-strong);
  font-size: calc(var(--fs-small) * 0.85);
  white-space: nowrap;
  opacity: 1;
}

.board.is-debug .tile { cursor: crosshair; }
.board.is-debug .board-index { display: none; }


/* -- 7  Grid mode ------------------------------------------------------------ */
/*
   Below 900px board.js switches the board to .is-grid: four columns in
   reading order, footprints capped at 2 × 2 (decoration at 1 × 1), written
   inline as grid spans, no hover engine. This is only the structure that
   makes the mode work; mobile.css owns its look.
*/

.board.is-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: var(--cell);
  grid-auto-flow: row dense;
  width: 100%;
}

/* mobile.css shows the paper; with no tracks here its lines are a
   gradient repeating on the cell, and the line elements are not drawn. */
.board.is-grid .board-paper {
  display: none;
  background-image:
    linear-gradient(to right,  var(--line-soft) var(--hair), transparent var(--hair)),
    linear-gradient(to bottom, var(--line-soft) var(--hair), transparent var(--hair));
  background-size: var(--cell) var(--cell);
}
.board.is-grid .board-lines { display: none; }

.board.is-grid .tile {
  position: static;
  width: auto;
  height: auto;
  translate: none;
}

/* The grid packs tiles with negative margins (mobile.css), which already
   reach each one a hairline over its neighbour; the inner layer must not
   reach again. */
.board.is-grid .tile-in { width: 100%; height: 100%; }

/* A static caption inside project tiles; logos and the blog are their own
   caption. mobile.css sizes the strip. */
.board.is-grid .tile-cap {
  height: auto;
  opacity: 1;
  transition: none;
}
.board.is-grid .tile-cap-in { width: auto; height: auto; }
.board.is-grid .tile-why { display: none; }
.board.is-grid .tile--logo .tile-cap,
.board.is-grid .tile--blog .tile-cap,
.board.is-grid .tile--post .tile-cap { display: none; }
.board.is-grid .tile--logo .tile-media { justify-content: center; }
