/* © 2026 Created with ♥ by Alex Munteanu | alexmunteanu.com */

/*
 * The token layer of the WADA VFX design system.
 *
 * THIS IS THE ONLY FILE IN THE PROJECT THAT CONTAINS A COLOUR VALUE. Every other
 * stylesheet - shared, published site, admin tool - consumes var(--...). That is not a style
 * preference: the local admin tool and the published gallery have to show the operator and
 * the client the same picture, and a second place to write a colour is the first place they
 * drift apart. It is also an invariant with a test behind it, so a literal added anywhere
 * else fails the suite rather than shipping.
 *
 * ONE CONSUMPTION PATH, AND IT RUNS ONE WAY.
 *
 *   a rule  ->  a semantic name  ->  a palette value  ->  a literal
 *
 * A rule in any stylesheet reads a SEMANTIC name: --surface-page, --text-meta,
 * --border-hairline. A semantic name reads a PALETTE value: --field, --ink-muted, --rail. A
 * palette value holds the literal and is read by nothing but the semantic block below it.
 *
 * No rule anywhere reads a palette value directly. The reason is not tidiness: with both
 * vocabularies live, half the product ends up on --rail and half on --border-hairline, and
 * the next person to change the hairline changes one of them. The palette answers "what
 * colour is this"; the semantic name answers "what job does this do", and a rule is always
 * asking the second question.
 *
 * ONE EXCEPTION, AND IT IS THE POINT OF THE EXCEPTION. The three fenced admin values have no
 * semantic alias and are read straight from the palette by the admin tool's own rules. An
 * alias would let a fenced value be spent under a neutral name - --status-warn on a client
 * surface reads as ordinary chrome, --admin-warn does not - and making the fence invisible at
 * the call site is exactly the failure the fence exists to prevent.
 *
 * There is deliberately NO LIGHT THEME and no prefers-color-scheme block, and none may be
 * derived. A light field around a graded frame shifts how a client reads exposure and
 * contrast, which would misrepresent the studio's work. DESIGN.md section 2.
 *
 * There is NO SHADOW TOKEN and none may be added - no shadow, no glow, no glass, no blur, no
 * border-plus-shadow. Depth is two steps of surface and a hairline, and nothing else.
 * DESIGN.md section 6.
 *
 * The contrast figures below were computed from these exact hex values, not estimated.
 * Changing a hex without re-measuring the pair it belongs to breaks an accessibility claim
 * the design makes.
 */

:root {
  /* ==== Palette =====================================================================
   * Achromatic. R=G=B on every one of these, so the chrome carries zero hue and the only
   * colour on a client-facing surface came out of a camera. Hierarchy is carried by
   * luminance, scale, case, tracking, weight and position, never by hue - there is no brand
   * colour and no accent, by design rather than by omission.
   *
   * Read only by the semantic block further down. Never by a rule.
   *
   * Computed contrast, WCAG 2.x, sRGB relative luminance:
   *   --ink on --field         16.81:1   (floor 4.5)
   *   --ink on --raised        15.46:1   (floor 4.5)
   *   --ink-muted on --field    6.99:1   (floor 4.5)
   *   --ink-muted on --raised   6.43:1   (floor 4.5)
   *   --edge on --field         3.86:1   (floor 3.0, non-text)
   *   --focus on --field       19.68:1   (floor 3.0, non-text)
   *   --buffered on --rail      3.16:1   (floor 3.0, non-text)
   *   --rail on --field         1.37:1   DECORATIVE BY DESIGN - see the note on --rail
   *   --raised on --field       1.09:1   a surface, not a boundary
   */

  /* The masking. Every frame sits on this, and it is the page background everywhere.
   * #0B0B0B rather than pure black because on an OLED a true-zero field switches pixels off
   * and the frame edge becomes a device artefact rather than masking. Lifted only slightly,
   * because the field must stay darker than the black inside any frame - which is what makes
   * a frame edge self-defining and why no frame ever needs a border. */
  --field: #0B0B0B;

  /* The only lifted surface, and the whole elevation scale: transport bar, dialog body,
   * input fill, the rectangle a poster loads into. There is no third step. */
  --raised: #161616;

  /* The structural hairline: the strip bench, dividers, the scrub track. At 1.37:1 it is
   * decorative by design and MUST NEVER be the sole boundary of anything operable. */
  --rail: #2A2A2A;

  /* The boundary of anything operable: buttons, inputs, drop zones. One pixel, always. */
  --edge: #6E6E6E;

  --ink: #EDEDED;        /* primary text, transport fill, icon strokes */
  --ink-muted: #9A9A9A;  /* edge codes and all technical metadata */
  --buffered: #767676;   /* the buffered range in the scrub track, and nothing else */

  /* The focus ring, and the only pure white in the system. Never text, never a fill, never
   * a border on its own. */
  --focus: #FFFFFF;

  /* ---- Fenced non-brand values --------------------------------------------------
   * NOT PART OF THE PALETTE. Do not fold these into the brand, do not use them as accents,
   * do not derive anything from them. They exist because the local admin tool has to report
   * transcode and publish state, and the client gallery shows none of them - the one
   * exception being --admin-fail, which reaches a client surface only as an input error
   * border.
   *
   * Every one of them is always paired with a glyph and a word. Colour alone never carries
   * meaning here.
   *
   *   --admin-ok on --field    7.43:1
   *   --admin-warn on --field  7.16:1
   *   --admin-fail on --field  5.46:1
   */
  --admin-ok: #5FAF7F;
  --admin-warn: #C9922E;
  --admin-fail: #DA6156;

  /* ==== Colour, semantic ============================================================
   * The names every rule in the product reads. Each one names a job.
   */
  --surface-page: var(--field);
  --surface-raised: var(--raised);
  --surface-poster: var(--raised);   /* the rectangle a poster loads into, at the clip's exact aspect */

  --text-primary: var(--ink);
  --text-meta: var(--ink-muted);
  --text-inverse: var(--field);      /* a label sitting on an --ink fill */

  --border-hairline: var(--rail);    /* never the sole boundary of anything operable */
  --border-operable: var(--edge);
  --border-strong: var(--ink-muted); /* input hover, ghost-button active */
  --ring-focus: var(--focus);

  --fill-primary: var(--ink);        /* primary button, selection */
  --fill-transport: var(--ink);      /* the played range of the scrub track */
  --fill-buffered: var(--buffered);
  --track-scrub: var(--rail);

  /* The two values that are not aliases of anything. Transparency appears exactly twice in
   * this system and this is both occurrences.
   *
   * --scrim-frame is the minimum neutral black that keeps the hover play glyph legible over
   * a blown-white frame. Neutral black only; never coloured, never branded.
   *
   * --player-backdrop is 97%, not opaque, and the 3% is load-bearing: the strip stays
   * faintly present behind the player, so the player reads as a frame lifted off the strip
   * rather than as a navigation away from it. */
  --scrim-frame: rgba(11, 11, 11, 0.72);
  --player-backdrop: rgba(11, 11, 11, 0.97);

  /* ==== Type =======================================================================
   * Two faces, four steps, ratio 1.333 from a 12px base. There is no fifth step: a fifth
   * step would be a headline, and nothing in a gallery may compete with a frame.
   *
   * The faces are self-hosted and SHA-256 pinned; the @font-face rules are in fonts.css.
   * Inter, Roboto, Arial, system-ui and every other default are excluded from both stacks on
   * purpose - a system face is the sound of an unbranded app, and this product has to sound
   * like a studio's own tool.
   *
   * STYLE FROM THE ROLE CLASSES IN base.css - .wv-edge-code .wv-title .wv-client
   * .wv-wordmark .wv-prose - never from --font-display / --font-mono alone. The classes
   * carry family, size, case, tracking and figure treatment TOGETHER, and a partial copy
   * loses one of them silently.
   */

  /* Names: wordmark, client headers, clip titles, buttons, prose. */
  --font-display: "Schibsted Grotesk", sans-serif;

  /* Codes: durations, resolutions, aspects, frame rates, counts, frame indices, status.
   * A brand device here, not a fallback. ui-monospace sits in front of the generic keyword so
   * a face that has not arrived yet falls back to the platform's own mono rather than to
   * whatever `monospace` happens to be mapped to. */
  --font-mono: "DM Mono", ui-monospace, monospace;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-bold: 700;    /* wordmark and nothing else */

  /* Steps 12 / 16 / 21 / 28. 21.31 and 28.41 are rounded to whole pixels. */
  --type-edge: 12px;
  --leading-edge: 18px;
  --tracking-edge: 0.08em;

  --type-title: 16px;
  --leading-title: 24px;
  --tracking-title: 0;

  --type-client: 21px;
  --leading-client: 24px;
  --tracking-client: 0.05em;

  --type-wordmark: 28px;
  --leading-wordmark: 30px;
  --tracking-wordmark: 0.16em;

  --measure-title: 32ch;  /* a clip title clamps to two lines at this measure */
  --measure-prose: 48ch;  /* the login gate and the empty states - the only continuous prose */

  --feature-caps: "case" 1;  /* raises hyphens, parens and slashes in every uppercase run */
  --feature-tnum: "tnum" 1;  /* any figure sitting in a column */

  /* ==== Space ======================================================================
   * Base unit 6px, and it is derived from the type rather than chosen: the edge code is set
   * 12/18, and 18 is three units. Deliberately not 8 - the 8px convention is the strongest
   * single source of cross-project sameness in software - and deliberately not 4.
   *
   * The step names are UNITS, not pixels: --space-12 is twelve units, which is 72px.
   */
  --space-1: 6px;
  --space-2: 12px;
  --space-3: 18px;
  --space-4: 24px;
  --space-6: 36px;
  --space-8: 48px;
  --space-12: 72px;
  --space-16: 96px;

  --frame-gap: var(--space-1);       /* frames nearly touch, so one row reads as one object */
  --row-gap: var(--space-12);        /* the only generosity in the layout: client to client */
  --header-to-rail: var(--space-3);  /* one edge-code leading */
  --hairline: 1px;
  --hit-min: 48px;                   /* eight units; clears the 44px practical minimum */

  /* Radius 0, everywhere, no exceptions: frames, buttons, inputs, the scrub handle, dialogs,
   * the drag ghost. A corner radius is a shape imposed on a frame, and it is the smallest
   * possible crop. This is the single fastest way to tell whether an output is on-system. */
  --radius: 0;

  /* ---- The module ---------------------------------------------------------------
   * The layout is not a column grid. The module is the ROW HEIGHT, every frame's width is
   * that height times the clip's own aspect ratio, and the only alignment in the whole page
   * is one hard left edge at the page margin.
   *
   * These two are redeclared at the breakpoints below, which is why they are the last thing
   * in this block. Nothing else in the token layer is width-dependent except the type scale.
   */
  --frame-height: 216px;             /* 36 units */
  --page-margin: var(--space-4);

  /* ==== Motion =====================================================================
   * One orchestrated moment on arrival, then near-total stillness. A gallery of moving
   * images cannot also have a moving interface.
   *
   * Only transform and opacity are ever animated, and in this system that prohibition has
   * teeth: a frame's width IS data, so animating it would animate a lie about the clip's
   * aspect ratio.
   *
   * Nothing exceeds 240ms. There is no bounce, no spring and no overshoot - the easing set is
   * one curve.
   */
  --dur-state: 120ms;      /* state feedback */
  --dur-enter: 240ms;      /* entrance */
  --dur-transport: 200ms;  /* transport-bar fade */
  --stagger-row: 40ms;     /* capped at the first two rows */
  --ease-out: cubic-bezier(0, 0, 0.2, 1);

  --settle-distance: 12px; /* two units, leftward: a strip coming to rest in a gate */
  --opacity-hover: 0.88;
  --opacity-active: 0.76;
  --opacity-frame-active: 0.96;

  /* Read once by the player at construction, so the token is the real value rather than a
   * number in a comment beside a hardcoded one. */
  --transport-idle: 1500ms;
}

/*
 * The type scale drops one step below 840px, where the layout also inverts.
 *
 * --type-edge is NOT in this block and stays 12px at every width. A spec readout is a
 * measurement, and a measurement that changes size between a laptop and a phone stops being
 * comparable across the devices a producer reads it on.
 *
 * It lives here rather than beside the role classes in base.css so that every token value in
 * the product, responsive or not, is in this one file.
 */
@media (max-width: 839px) {
  :root {
    --type-client: 16px;
    --leading-client: 21px;
    --type-wordmark: 21px;
    --leading-wordmark: 24px;
    --type-title: 12px;
    --leading-title: 18px;
  }
}

/*
 * The row height, and the page margin that the strip bleeds past.
 *
 * There is no 840px step. The design system's own spacing file carries one that redeclares
 * 216px, which is the value already in force, so it is dropped here rather than copied: a
 * media query that changes nothing is a breakpoint a reader has to check and discard.
 *
 * 1160 is where the margin widens with the frames. DESIGN.md's section 9 arithmetic
 * (2 x 384 + 6 + 48 = 822) is what fixes the margin at 24px until then.
 */
@media (min-width: 1160px) {
  :root {
    --frame-height: 300px;
    --page-margin: var(--space-6);
  }
}

@media (min-width: 1460px) {
  :root {
    --frame-height: 384px;
  }
}
