/* ==========================================================================
   Dot matrix 404 1.0 — a dot-matrix display that spells a short message
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   The message lives in the DOM as real text. Without JavaScript the browser
   still shows it, screened through a dot mask in pure CSS; once the script
   runs, a <canvas> takes over and paints the same grid to the pixel. The text
   node is never removed — it stays selectable, translatable and readable by
   assistive technology either way.

   PROPORTIONS
   This component has no body text, so nothing here derives from a font size.
   Everything derives from ONE length: the grid cell, which is the width of
   the display divided by --dm-columns. The ratios below were measured on the
   reference at seven widths, from 272 to 920 px:

     cell            width / 42        (42 columns, whatever the width)
     rows            floor(height / cell)
     dot, off        0.616 x cell      (r/cell = 0.3061 .. 0.3099)
     dot, on         0.668 x cell      (r/cell = 0.3310 .. 0.3353)
     glyph height    12 cells          (constant at every width measured)
     glyph advance   11 cells for "4", 8 for the rest, 2 between glyphs

   So --dm-columns alone rescales the whole display, and nothing else needs
   to be touched.

   Order of this file:
     1. Design tokens
     2. Block
     3. Canvas (the live rendering)
     4. Text: dot-screened fallback, then visually hidden once enhanced
     5. Dust
     6. Veil
     7. Variants
     8. Fallbacks: reduced motion, forced colours, no mask support

   Naming follows BEM: .dot-matrix, .dot-matrix__part, .dot-matrix--variant.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

.dot-matrix {
  /* --- Grid ------------------------------------------------------------- */

  /* The one number that scales everything. The reference holds 42 columns at
     every width, and derives the cell from it — not the other way round. */
  --dm-columns: 42;

  /* Dot diameters, as a fraction of a cell. Lit dots are ~8% fatter than
     unlit ones: that difference is what gives the glyph its weight, and it
     held on all four widths where it could be measured reliably. */
  --dm-dot: 0.616;
  --dm-dot-active: 0.668;

  /* --- Colour ----------------------------------------------------------- */
  --dm-off: #f0f0f0; /* the resting grid */
  --dm-on: #262626; /* a dot inside a glyph */
  --dm-veil: #ffffff; /* what the display fades into, bottom edge */

  /* --- Edge thinning ---------------------------------------------------- */
  /* The grid does not fade out at the edges, it thins out: dots are dropped
     entirely, never dimmed. Both numbers are a normalised distance from the
     centre, where 1 is the mid-edge and 1.41 the corner.
       --dm-fade-start  every dot survives inside this radius
       --dm-fade-end    share of dots still standing at the far corner */
  --dm-fade-start: 0.86;
  --dm-fade-end: 0.52;

  /* Same seed, same holes: the reference draws an identical pattern on every
     reload, and so does this. Change the number for a different scatter. */
  --dm-seed: 1;

  /* --- Pointer ---------------------------------------------------------- */
  /* Dots back away from the pointer. Measured on the reference at four pointer
     positions, 2868 samples, against the same display with no pointer on it:
     the push is radial, strongest under the pointer, and it dies out linearly.

       distance   0-60px  60-120  120-180  180-250  250-350  beyond
       measured   +2.48   +2.21   +1.65    +1.01    +0.41    ~0
       model      +2.54   +2.10   +1.63    +1.10    +0.39     0

     Fitted by least squares: 2.880 px of push over 345 px of reach, which is
     0.112 of a cell over 13.42 cells — in cells, because everything else here
     is. RMS 0.120 px. Set --dm-push to 0 to pin the grid. */
  --dm-push: 0.112;
  --dm-push-radius: 13.42;
  /* Time constant of the easing that follows the pointer. Not a duration: the
     distance left is cut by about 63% every --dm-push-ease. */
  --dm-push-ease: 120ms;

  /* --- Dust ------------------------------------------------------------- */
  --dm-dust-count: 36;
  --dm-dust-size-min: 4px;
  --dm-dust-size-max: 6px;
  --dm-dust-scale-max: 1.2;
  --dm-dust-opacity-min: 0.33;
  --dm-dust-opacity-max: 0.64;
  --dm-dust-speed-min: 2; /* px per second */
  --dm-dust-speed-max: 22;
  --dm-dust-blur: 1px;
  /* Sampled 16 / 12 / 8 times respectively on the reference. */
  --dm-dust-tint-a: #ffffff;
  --dm-dust-tint-b: #cccccc;
  --dm-dust-tint-c: #999999;

  /* --- Shape ------------------------------------------------------------ */
  /* The reference sits in a 540x250 box. Set a height instead if you prefer;
     the row count follows the height on its own. */
  --dm-ratio: 54 / 25;

  /* --- Motion ----------------------------------------------------------- */
  --dm-fade-in: 480ms; /* the display appearing once painted */
  --dm-fade-in-delay: 0ms;
  --dm-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ==========================================================================
   2. Block
   ========================================================================== */

.dot-matrix {
  /* The component states its own box model rather than trusting the page. */
  box-sizing: border-box;
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--dm-ratio);
  overflow: hidden;

  /* Container queries give the children a length equal to a share of THIS
     box, which is what the CSS fallback needs to size its dot mask. An
     element cannot query itself, hence the work happening in the children. */
  container-type: inline-size;
  container-name: dot-matrix;
}

.dot-matrix *,
.dot-matrix *::before,
.dot-matrix *::after {
  box-sizing: border-box;
}

/* One cell, expressed against the width of the block. Declared on the
   children — pseudo-elements included — because 100cqw written on
   .dot-matrix itself would resolve against its parent, not against itself. */
.dot-matrix::before,
.dot-matrix__canvas,
.dot-matrix__text,
.dot-matrix__dust {
  --dm-cell: calc(100cqw / var(--dm-columns));
}

/* ==========================================================================
   3. Canvas — the live rendering
   ========================================================================== */

.dot-matrix__canvas {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: block;
  width: 100%;
  height: 100%;
  /* Nothing is painted until the script has measured the box, so the element
     stays invisible rather than flashing an empty rectangle. */
  opacity: 0;
}

.dot-matrix--live .dot-matrix__canvas {
  opacity: 1;
  transition: opacity var(--dm-fade-in) var(--dm-ease) var(--dm-fade-in-delay);
}

/* ==========================================================================
   4. Text
   --------------------------------------------------------------------------
   Two jobs, one node. Before the script runs it is the display itself, screened
   through a dot mask so it reads as a matrix rather than as plain type. After,
   the canvas draws the real thing and the text steps aside — visually, not
   from the accessibility tree.
   ========================================================================== */

/* The resting grid behind the fallback text. Unlit dots, thinning towards the
   edges — a smooth fade here, where the canvas drops whole dots; the CSS
   fallback cannot draw lots, and at this size the eye reads them the same.
   It is a pseudo-element of the block rather than of the text, so the dot mask
   on the text below cannot reach it. */
.dot-matrix::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: radial-gradient(
    circle at center,
    var(--dm-off) calc(var(--dm-dot) * 50%),
    transparent calc(var(--dm-dot) * 50% + 0.5px)
  );
  background-size: var(--dm-cell) var(--dm-cell);
  background-position: center;
  --dm-thin: radial-gradient(
    farthest-corner at center,
    #000 calc(var(--dm-fade-start) * 70%),
    rgb(0 0 0 / var(--dm-fade-end)) 100%
  );
  -webkit-mask-image: var(--dm-thin);
  mask-image: var(--dm-thin);
}

/* Enhanced: the canvas paints the grid, this one steps out of the way. */
.dot-matrix--live::before {
  content: none;
}

.dot-matrix__text {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;

  /* 12 cells of cap height. Digits sit at roughly 0.72 em in the grotesques
     browsers fall back to, hence the divisor. */
  font-family: var(--dm-font, ui-sans-serif, system-ui, sans-serif);
  font-size: calc(var(--dm-cell) * 12 / 0.72);
  font-weight: 700;
  line-height: 1;
  letter-spacing: calc(var(--dm-cell) * 0.5);
  /* Keep the last letter's spacing from pushing the word off centre. */
  text-indent: calc(var(--dm-cell) * 0.5);
  color: var(--dm-on);
  white-space: nowrap;
  user-select: none;

  /* The dot screen. Radius is --dm-dot-active/2 of a cell, same as the lit
     dots the canvas paints. */
  --dm-screen: radial-gradient(
    circle at center,
    #000 calc(var(--dm-dot-active) * 50%),
    transparent calc(var(--dm-dot-active) * 50% + 0.5px)
  );
  -webkit-mask-image: var(--dm-screen);
  mask-image: var(--dm-screen);
  -webkit-mask-size: var(--dm-cell) var(--dm-cell);
  mask-size: var(--dm-cell) var(--dm-cell);
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
}

/* Enhanced: the canvas is the display, the text only has to stay readable to
   assistive technology. Not display:none, which would take it out of the
   accessibility tree along with the pixels. */
.dot-matrix--live .dot-matrix__text {
  width: 1px;
  height: 1px;
  inset: auto auto 0 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  -webkit-mask-image: none;
  mask-image: none;
}

/* ==========================================================================
   5. Dust
   ========================================================================== */

.dot-matrix__dust {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  /* Motes are placed in percentages of this box, so it must not scroll. */
  overflow: hidden;
}

.dot-matrix__mote {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 50%;
  filter: blur(var(--dm-dust-blur));
  will-change: transform;
}

/* ==========================================================================
   6. Veil
   --------------------------------------------------------------------------
   Transparent at the top, solid at the bottom. This is what produces the
   gradient one thinks one sees inside the canvas: the grid itself is two
   flat colours and nothing else.
   ========================================================================== */

.dot-matrix::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background-image: linear-gradient(
    to bottom,
    transparent 0%,
    var(--dm-veil) 100%
  );
}

/* ==========================================================================
   7. Variants
   ========================================================================== */

/* Reversed: a light matrix on a dark page. */
.dot-matrix--dark {
  --dm-off: #1c1c1c;
  --dm-on: #f2f2f2;
  --dm-veil: #090909;
  --dm-dust-tint-a: #000000;
  --dm-dust-tint-b: #333333;
  --dm-dust-tint-c: #666666;
}

/* No veil, when the display sits in the middle of a section rather than at
   the bottom of a hero. */
.dot-matrix--flat::after {
  content: none;
}

/* Denser grid, for a small box where 42 columns would swallow the glyph. */
.dot-matrix--compact {
  --dm-columns: 30;
}

/* ==========================================================================
   8. Fallbacks
   ========================================================================== */

/* Motion. Delays count as motion spread over time, so they go to zero too. */
@media (prefers-reduced-motion: reduce) {
  .dot-matrix {
    --dm-fade-in: 0ms;
    --dm-fade-in-delay: 0ms;
    /* The script reads these and leaves every mote where it was placed, and
       the grid still. Nothing follows the pointer either. */
    --dm-dust-speed-min: 0;
    --dm-dust-speed-max: 0;
    --dm-push: 0;
  }

  .dot-matrix--live .dot-matrix__canvas {
    transition: none;
  }
}

/* Forced colours. The canvas is a picture to the OS, so it is left alone and
   the honest fallback comes back: real text, no mask, system colours. */
@media (forced-colors: active) {
  .dot-matrix__canvas,
  .dot-matrix__dust {
    display: none;
  }

  .dot-matrix::before,
  .dot-matrix::after {
    content: none;
  }

  .dot-matrix--live .dot-matrix__text,
  .dot-matrix__text {
    position: static;
    display: flex;
    width: auto;
    height: 100%;
    clip-path: none;
    overflow: visible;
    color: CanvasText;
    forced-color-adjust: none;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

/* No mask support: showing the message as plain type beats showing a blank
   box. Only reachable without JavaScript, since the canvas covers this. */
@supports not ((-webkit-mask-image: radial-gradient(#000, #000)) or (mask-image: radial-gradient(#000, #000))) {
  .dot-matrix__text {
    -webkit-mask-image: none;
    mask-image: none;
  }
}
