/* ==========================================================================
   ASCII fluid 1.0 — a picture redrawn as a field of ASCII glyphs
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   The component takes what sits inside .ascii-fluid__source — a real <img>,
   or a real line of text — and repaints it every frame as ASCII, lit by a
   trail that follows the pointer. Before the script runs, or if it never
   runs, that source is what the page shows: the picture itself, centred.

   PROPORTIONS
   One variable rescales everything: --af-glyph-size. The glyph, the cell, the
   control panel and its type all derive from it, so the component keeps its
   look at any size. The ratios come from the published reference, measured
   frame by frame (see README.md, "Provenance"):

     glyph size          1                (the reference unit, 16 px)
     drawn glyph         0.92             (the cell breathes around the letter)
     cell width          1 / density      (never under 3 px)
     cell height         1.35 / density   (never under 4 px)
     panel type          0.6875           (11 px when the glyph is 16 px)

   And inside the panel, everything is in em of that 0.6875 — measured on the
   reference, which draws it at 11 px:

     toggle height       2.5455 em        (28 px)
     toggle padding      0 0.9091 em      (0 10 px)
     panel width         19.0910 em       (210 px)
     panel padding       0.9091 em        (10 px)
     panel offset        0.5455 em        (6 px below the toggle)
     panel shadow        0.2728 em        (3 px, hard, no blur)
     field gap           0.4546 em        (5 px between label and control)
     field spacing       0.9091 em        (10 px between fields)
     colour input        2.5455 em tall   (28 px)
     slider box          1.2728 em        (14 px)
     slider track        0.1819 em        (2 px)
     slider thumb        0.9091 em        (10 px)
     panel inset         0.9091 em        (10 px from the top left corner)

   Ratios are rounded up, never down: the engine truncates to a 64th of a
   pixel, and a ratio rounded down invents a one-pixel overflow.

   Order of this file:
     1. Face declaration
     2. Design tokens
     3. Shell, canvas, source
     4. The probes
     5. Control panel: toggle and panel
     6. Control panel: fields, sliders, swatches
     7. Variants
     8. Fallbacks: reduced motion, forced colours

   Naming follows BEM: .ascii-fluid, .ascii-fluid__part, .ascii-fluid--variant.
   ========================================================================== */

/* ==========================================================================
   1. Face declaration
   --------------------------------------------------------------------------
   Local, and only useful to .ascii-fluid--text: the glyph grid itself is
   drawn in the system monospace stack, which costs no request at all. Jaro is
   what the reference sets its wordmark in — latin subset, the only one the
   demo needs. See README.md, "Credits".
   ========================================================================== */

@font-face {
  font-family: "Jaro";
  src: url("Jaro-400-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   2. Design tokens
   --------------------------------------------------------------------------
   Every colour, length and duration the script needs is declared here and
   read back off a probe (see part 3). Bare numbers — density, contrast, trail
   strength, pointer radius — are not lengths, so they are data-* attributes
   on the element, not custom properties.
   ========================================================================== */

.ascii-fluid {
  /* The module. Everything else derives from it. */
  --af-glyph-size: 16px;
  --af-line-size: calc(1.35 * var(--af-glyph-size));

  --af-color: #ffffff;
  --af-background: #0a0a0a;

  /* Share of the box taken by the subject. A percentage, a calc() or a
     length: the probe resolves it in every case. */
  --af-mask-size: 60%;

  /* Interval between two frames: the first on a comfortable device, the
     second on a modest one. 25 ms is 40 frames per second — beyond that the
     cost rises without the eye gaining anything. */
  --af-frame-interval: 25ms;
  --af-frame-interval-modest: 41.667ms;

  /* With data-motion="drift", how long the component has to be idle before
     the drift takes over again after a gesture. The hand always comes
     first. */
  --af-drift-resume: 1600ms;

  /* The system stack: no request at all, and a fixed pitch everywhere. */
  --af-font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;

  /* The settings panel, at the scale of the module. */
  --af-ui-size: calc(0.6875 * var(--af-glyph-size));
  --af-ui-color: var(--af-color);
  --af-ui-background: var(--af-background);
  --af-ui-focus: var(--af-color);

  /* Size of the text when the subject is text rather than an image. */
  --af-text-size: calc(22 * var(--af-glyph-size));
}

/* ==========================================================================
   3. Shell, canvas, source
   ========================================================================== */

/* The component assumes no reset: it lays down its own, and only at home. */
.ascii-fluid {
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
  display: block;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  background: var(--af-background);
  color: var(--af-color);
}

/* border-box spelled out, and not "inherit": Chromium does not let a
   <summary> inherit box-sizing, so it stays in content-box and gains two
   pixels of border. Measured, not assumed — see README.md, "Departures". */
.ascii-fluid *,
.ascii-fluid *::before,
.ascii-fluid *::after {
  box-sizing: border-box;
}

.ascii-fluid__canvas {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  /* pan-y, not none: the finger drives the effect but the page still
     scrolls. This is the only deliberate behavioural departure from the
     reference, which blocks scrolling over its whole height. */
  touch-action: pan-y;
}

.ascii-fluid[data-touch="lock"] .ascii-fluid__canvas {
  touch-action: none;
}

/* The subject. Visible until the script takes over: it is the rendering
   without JavaScript, and it is also what assistive technology reads once the
   canvas is in place. */
.ascii-fluid__source {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  padding: 0;
  text-align: center;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  font-family: var(--af-font-family);
  font-size: var(--af-text-size);
  line-height: 1.1;
  color: var(--af-color);
}

.ascii-fluid__source img {
  /* Same framing as the mask: the subject fits in a square of --af-mask-size
     and keeps its proportions. The ASCII rendering lands exactly on top of the
     fallback. */
  max-inline-size: var(--af-mask-size);
  max-block-size: var(--af-mask-size);
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
}

/* The script has taken over: the subject leaves the picture without leaving
   the accessibility tree. Neither display:none nor visibility:hidden, which
   would remove it from it. */
.ascii-fluid--live .ascii-fluid__source {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ==========================================================================
   4. The probes
   --------------------------------------------------------------------------
   getComputedStyle does not resolve a custom property: it hands back the
   declaration, `calc(0.6875 * 16px)` where `11px` is needed. The values are
   therefore carried by an element that is invisible but does occupy a box —
   never display:none, which would have none at all.

   Two probes, because a percentage does not behave the same way depending on
   the property: `width` resolves against the component (which is what
   --af-mask-size means), `min-width` stays an absolute length (which is what
   --af-glyph-size means).
   ========================================================================== */

.ascii-fluid__probe {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  visibility: hidden;
  pointer-events: none;
  min-inline-size: var(--af-glyph-size);
  min-block-size: var(--af-line-size);
  inline-size: 0;
  block-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
  font-family: var(--af-font-family);
  color: var(--af-color);
  background-color: var(--af-background);
  transition-property: opacity, transform;
  transition-duration: var(--af-frame-interval), var(--af-frame-interval-modest);
  transition-delay: var(--af-drift-resume);
}

.ascii-fluid__probe--relative {
  min-inline-size: 0;
  min-block-size: 0;
  inline-size: var(--af-mask-size);
}

/* ==========================================================================
   5. Control panel: toggle and panel
   --------------------------------------------------------------------------
   <details>/<summary> already gives a labelled toggle, keyboard operation and
   the right role. The panel is only built when data-controls="show".
   ========================================================================== */

.ascii-fluid__controls {
  position: absolute;
  inset-block-start: 0.9091em;
  inset-inline-start: 0.9091em;
  z-index: 5;
  font-family: var(--af-font-family);
  font-size: var(--af-ui-size);
  line-height: 1.2;
  color: var(--af-ui-color);
  -webkit-user-select: none;
  user-select: none;
}

.ascii-fluid__toggle {
  display: inline-flex;
  align-items: center;
  block-size: 2.5455em;
  padding-inline: 0.9091em;
  border: 1px solid currentColor;
  border-radius: 0;
  background: var(--af-ui-background);
  color: inherit;
  font: inherit;
  cursor: pointer;
  /* The default arrow of <summary> does not sit well with a square panel: it
     is removed, and the sign is written instead. */
  list-style: none;
}

.ascii-fluid__toggle::-webkit-details-marker {
  display: none;
}

/* The sign restates, for the eye, the state aria-expanded already carries. */
.ascii-fluid__toggle::after {
  content: " +";
  white-space: pre;
}

.ascii-fluid__controls[open] > .ascii-fluid__toggle::after {
  content: " \2212"; /* typographic minus, same pitch as the plus */
}

.ascii-fluid__toggle:focus-visible {
  outline: 2px solid var(--af-ui-focus);
  outline-offset: 2px;
}

.ascii-fluid__panel {
  inline-size: 19.0910em;
  margin-block-start: 0.5455em;
  padding: 0.9091em;
  border: 1px solid currentColor;
  background: var(--af-ui-background);
  box-shadow: 0.2728em 0.2728em 0 currentColor;
}

/* ==========================================================================
   6. Control panel: fields, sliders, swatches
   ========================================================================== */

.ascii-fluid__fields {
  display: block;
}

.ascii-fluid__field {
  display: flex;
  flex-direction: column;
  gap: 0.4546em;
  margin-block-end: 0.9091em;
  color: inherit;
}

.ascii-fluid__field:last-child {
  margin-block-end: 0;
}

/* 1.7273 em = 19 px, the height a checkbox takes on the reference with its
   native margins. It is set on the row rather than recreating those margins:
   the result then no longer depends on the engine. */
.ascii-fluid__field--inline {
  flex-direction: row;
  align-items: center;
  gap: 0.7273em;
  block-size: 1.7273em;
}

/* The name and the figure make a single line — "Density 10.00" — as on the
   reference, where they are one single piece of text. */
.ascii-fluid__field-head {
  display: flex;
  align-items: baseline;
  gap: 0.3637em;
}

.ascii-fluid__field-value {
  font-variant-numeric: tabular-nums;
}

.ascii-fluid__select,
.ascii-fluid__color,
.ascii-fluid__range,
.ascii-fluid__file,
.ascii-fluid__button {
  inline-size: 100%;
  min-inline-size: 0;
  font: inherit;
  color: inherit;
  accent-color: currentColor;
}

/* Height set, not deduced: a <select> gives itself its own line height,
   taller than the one it inherits, and the box was growing by four pixels. */
.ascii-fluid__select {
  block-size: 1.7273em;
  padding-block: 0;
  padding-inline: 0.2728em;
  border: 1px solid currentColor;
  border-radius: 0;
  background: var(--af-ui-background);
  line-height: 1.2;
}

/* The subject can be replaced by a local file: the data becomes a data: URL,
   which the security policy of the site allows for images. Nothing goes out
   over the network. */
.ascii-fluid__file {
  padding: 0.3637em;
  border: 1px solid currentColor;
  border-radius: 0;
  background: var(--af-ui-background);
}

.ascii-fluid__button {
  block-size: 2.3637em;
  padding-inline: 0.4546em;
  border: 1px solid currentColor;
  border-radius: 0;
  background: var(--af-ui-background);
  cursor: pointer;
}

.ascii-fluid__file:focus-visible,
.ascii-fluid__button:focus-visible {
  outline: 2px solid var(--af-ui-focus);
  outline-offset: 2px;
}

.ascii-fluid__color {
  block-size: 2.5455em;
  padding: 0;
  border: 1px solid currentColor;
  border-radius: 0;
  background: var(--af-ui-background);
  cursor: pointer;
}

.ascii-fluid__check {
  inline-size: auto;
  margin: 0;
  accent-color: currentColor;
}

.ascii-fluid__select:focus-visible,
.ascii-fluid__color:focus-visible,
.ascii-fluid__check:focus-visible,
.ascii-fluid__range:focus-visible {
  outline: 2px solid var(--af-ui-focus);
  outline-offset: 2px;
}

/* The slider is redrawn on both sides — WebKit and Gecko share no
   pseudo-class for this. */
.ascii-fluid__range {
  -webkit-appearance: none;
  appearance: none;
  block-size: 1.2728em;
  padding: 0;
  margin: 0;
  border: none;
  background: transparent;
}

.ascii-fluid__range::-webkit-slider-runnable-track {
  block-size: 0.1819em;
  border: none;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 20%, transparent);
}

.ascii-fluid__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  inline-size: 0.9091em;
  block-size: 0.9091em;
  margin-block-start: -0.3637em;
  border: none;
  border-radius: 999px;
  background: currentColor;
  box-shadow: none;
}

.ascii-fluid__range::-moz-range-track {
  block-size: 0.1819em;
  border: none;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 20%, transparent);
}

.ascii-fluid__range::-moz-range-thumb {
  inline-size: 0.9091em;
  block-size: 0.9091em;
  border: none;
  border-radius: 999px;
  background: currentColor;
  box-shadow: none;
}

.ascii-fluid__range::-moz-focus-outer {
  border: 0;
}

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

/* The subject is text: it carries its own typography, which the engine reads
   back to draw the mask. Changing the face here changes the ASCII
   rendering. */
.ascii-fluid--text .ascii-fluid__source {
  font-family: "Jaro", var(--af-font-family);
  font-weight: 400;
  letter-spacing: calc(-0.1875 * var(--af-glyph-size));
}

/* A light panel over a dark rendering, or the other way round: the two
   interface tokens are set without touching the rendering. */
.ascii-fluid--panel-light {
  --af-ui-color: #111111;
  --af-ui-background: #ffffff;
}

/* ==========================================================================
   8. Fallbacks: reduced motion, forced colours
   ========================================================================== */

/* The engine freezes on its own under this query — there is no CSS
   transition to cut, but the panel does have some. A delay is movement spread
   over time: it goes too. */
@media (prefers-reduced-motion: reduce) {
  .ascii-fluid,
  .ascii-fluid * {
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
  }
}

/* In forced colours, the rendering follows the system theme: the probe
   resolves CanvasText and Canvas into real colours, and the engine paints with
   them. */
@media (forced-colors: active) {
  .ascii-fluid {
    --af-color: CanvasText;
    --af-background: Canvas;
    --af-ui-color: CanvasText;
    --af-ui-background: Canvas;
    --af-ui-focus: Highlight;
  }

  .ascii-fluid__toggle,
  .ascii-fluid__panel,
  .ascii-fluid__select,
  .ascii-fluid__color {
    border-color: CanvasText;
  }

  .ascii-fluid__panel {
    box-shadow: none;
  }

  .ascii-fluid__range::-webkit-slider-runnable-track,
  .ascii-fluid__range::-moz-range-track {
    background: CanvasText;
  }
}
