/* Security BSides Dehradun 0x03
   tokens.css: design tokens. Variables only.

   This file is the design contract. Do not add component or layout rules here.
   Do not hardcode a colour, font-size, font-family, duration or easing anywhere
   else in the codebase, reference these tokens.

   Cascade layer order is declared here, once, for the whole project. */

@layer tokens, base, components, sections, utilities;

/* Self-hosted Geist, added 2 September 2026. This follows the four-step path in
   docs/README.md rather than departing from it, including the CSP change at step
   three that the same note says people forget.

   Two variable files carry the whole 100 to 900 range, so the site makes two font
   requests instead of one per weight. 138K for both. Upright only: the five <em>
   on the entire site all sit inside body prose, where a synthetic oblique is not
   worth a third 68K file.

   font-display: swap means text paints immediately in the fallback face and
   swaps when the file lands, so a slow connection never waits on a blank page.

   Licensed under the SIL Open Font License 1.1, copyright Vercel in collaboration
   with basement.studio. The licence ships beside the fonts at
   assets/font/LICENSE.txt, which the OFL requires. Do not remove it. */
@font-face {
  font-family: Geist;
  src: url("../font/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("../font/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@layer tokens {
  /* Typeface stacks. Geist and Geist Mono are self-hosted and lead their stacks.
     The system faces behind them are what a reader sees for the few hundred
     milliseconds before the woff2 lands, and permanently if it never does.

     Helvetica Neue led --font-sans until 2 September 2026, which meant the site
     was designed in one face and read in another by most of its audience: Segoe
     UI on Windows, Roboto on Android. Almost nobody outside Apple hardware ever
     saw the grotesque this type was tuned against. Geist closes that gap, and it
     is free under the SIL Open Font License, so none of the licensing cost that
     ruled out a real Helvetica applies. See docs/DESIGN-BRIEF.md §2.1 for usage
     rules. */
  :root {
    --font-sans:
      Geist, "Helvetica Neue", Inter, -apple-system, BlinkMacSystemFont,
      "Segoe UI", Roboto, "Liberation Sans", Arial, sans-serif;

    --font-serif:
      ui-serif, "New York", "Iowan Old Style", "Palatino Linotype", Palatino,
      Georgia, "Times New Roman", serif;

    --font-mono:
      "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono",
      "Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;

    /* Fluid type scale. Never write a raw px font-size.

       The top four steps were pulled down on 29 August 2026: the display, the
       h1 and the lead read as too large on a laptop, and a 168px display over
       a 900px viewport left the home hero 1212px tall, so the page could not
       be seen at once. Ceilings now 128 / 68 / 50 / 24 instead of 168 / 88 /
       60 / 28. Each clamp still locks to its floor and its ceiling at the same
       viewport widths as before, so this is a change of size and not of tuning.
       Do not raise them back without looking at cfp.html at 1440 first: that
       title was three lines at 88px. */
    --fs-label:   0.6875rem;
    --fs-xs:      0.78125rem;
    --fs-sm:      0.875rem;
    --fs-base:    clamp(1rem,      0.965rem + 0.18vw,  1.0625rem);
    --fs-lg:      clamp(1.125rem,  1.05rem  + 0.36vw,  1.3125rem);
    --fs-xl:      clamp(1.25rem,   1.14rem  + 0.5vw,   1.5rem);
    --fs-2xl:     clamp(1.75rem,   1.42rem  + 1.55vw,  2.5rem);
    --fs-3xl:     clamp(2rem,      1.54rem  + 2.17vw,  3.125rem);
    --fs-4xl:     clamp(2.375rem,  1.46rem  + 4.29vw,  4.25rem);
    --fs-display: clamp(2.75rem,   0.83rem  + 8.25vw,  8rem);

    /* Tracking: large type tighter, small type looser. The biggest
       single "expensive" signal in the whole system. */
    --track-display: -0.038em;
    --track-3xl:     -0.028em;
    --track-2xl:     -0.022em;
    --track-xl:      -0.016em;
    --track-body:    -0.004em;
    --track-label:    0.13em;
    --track-mono:     0.01em;

    /* Line heights */
    --lh-display: 0.88;
    --lh-heading: 1.04;
    --lh-snug:    1.28;
    --lh-body:    1.62;
    --lh-prose:   1.72;

    /* Weights: grotesque display wants 500–700, never 800/900 */
    --fw-regular: 400;
    --fw-medium:  500;
    --fw-bold:    700;

    /* Measure */
    --measure:        68ch;
    --measure-lead:   62ch;
    --measure-narrow: 54ch;

    /* Space, grid, layout */
    --page-margin:      clamp(1.25rem, 5.5vw, 7rem);
    --container:        1440px;
    --container-narrow: 780px;
    --gutter:           clamp(1rem, 1.9vw, 1.75rem);
    --section-y:        clamp(3.5rem, 7.5vw, 8.5rem);

    /* The gap after a head, before the body it introduces. Three rules share
       it: .section-head, .plate--lead, and whatever element follows a
       .timeline. All three used to carry the same clamp written out longhand,
       two of them with a comment asking the next reader to keep them equal,
       which is the job of a token rather than of a comment.

       Both of these came down on 29 August 2026, with the type: --section-y
       from 72/168 to 56/136, --head-y from 48/96 to 32/64. Nothing was
       squeezed. The headline sizes fell about 24 percent and the space around
       them had to fall with them or the page reads as small type adrift in
       gaps cut for larger type. */
    --head-y:           clamp(2rem, 4vw, 4rem);
    --stack:            clamp(1rem, 2vw, 1.75rem);

    --s-1:  0.25rem;
    --s-2:  0.5rem;
    --s-3:  0.75rem;
    --s-4:  1rem;
    --s-5:  1.5rem;
    --s-6:  2rem;
    --s-7:  3rem;
    --s-8:  4rem;
    --s-9:  6rem;
    --s-10: 8rem;

    /* Radii: Swiss is sharp. Nothing above 2px except status pills. */
    --r-0:    0;
    --r-1:    2px;
    --r-pill: 999px;

    /* Hairlines are the primary structural device */
    --bw:        1px;
    --bw-strong: 2px;

    /* Elevation: overlays only. Page content never has a shadow. */
    --shadow-overlay: 0 24px 64px -24px oklch(18% 0.02 265 / 0.28);

    /* Motion */
    --dur-fast: 150ms;
    --dur:      300ms;

    /* --dur-slow is NOT "a slower --dur", and the distinction was made explicit
       on 2 September 2026 when --dur-entrance was cut. It is for the two things
       that travel a long way across the screen rather than arriving in place:
       the circular theme wipe, and the contour divider drawing itself from its
       left edge. A wipe the width of the viewport at entrance speed reads as a
       flicker. Anything that moves 14px belongs on --dur-entrance instead. */
    --dur-slow: 620ms;
    --ease:     cubic-bezier(0.2, 0.8, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

    /* --ease-spring overshoots, and that is the point: its second control point
       sits at y = 1.56, so whatever it drives passes its target and settles
       back. Two declarations in components.css have asked for this token since
       the theme toggle was written, both of them on the sun and moon glyphs, and
       both were quietly taking the `var(--ease-spring, var(--ease-out))`
       fallback because the token was never defined anywhere. Nothing was broken,
       which is exactly why it went unnoticed for so long.

       A rotate that arrives and stops is a switch. A rotate that arrives, goes a
       little past and comes back is a thing with mass, and that is the whole
       difference between the glyph swap reading as a mechanism and reading as a
       crossfade.

       Overshoot is safe on rotate and on scale. Do not put it on opacity or on a
       colour: both clamp at their own limits, so the overshoot is spent flat
       against the clamp and all you have bought is a duration that ends late. */
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

    /* The third curve on the site, and it exists because both of the others are
       wrong for something that travels a long way through readable content. It
       is --ease with its second control point pulled down from 0.8 to 0.35 and
       its third pushed out from 0.2 to 0.3, which is to say: spend less of the
       distance early.

       Solved numerically over the counter's worst case, eighteen digit faces in
       720ms, the way the --dur-entrance paragraph below insists on.

       --ease-out spends 97.2 percent of the travel in the first half of the
       duration and then gives the final face 424ms to slide into place. --ease is
       barely better at 94.6 percent and 364ms. Both produce the same two
       artefacts: seventeen faces as an unreadable smear, then one digit crawling
       the last half-cell for a third of a second, which does not read as a reel
       settling. It reads as a reel that has finished and a digit that is stuck.

       This curve spends 84 percent by the halfway point and gives the last face
       229ms, and it opens at 1.75 times its own average speed against 6.25 for
       --ease-out, so it starts as a spin rather than as a jump. First face is
       clear of the window at 21.5ms, so nothing hesitates.

       If you shorten --dur-reel, solve this again. The only figure that matters
       is the last face: under about 120ms it stops reading as a deceleration and
       the number appears to snap. */
    --ease-reel:   cubic-bezier(0.2, 0.35, 0.3, 1);

    /* Motion, part two: the choreography layer.
       The three durations above are for state changes: a hover, a colour, a
       border. These are for entrances and for scroll-driven drift, which is a
       different job, longer, always eased out, and always additive to a
       layout that is already correct without it.

       --stagger is the interval every entrance sequence counts in, the home
       hero enumerates nine steps of it. The mobile nav overlay has a stagger
       of its own at 40ms and deliberately does not use this token: it is a
       menu opening under the reader's finger, not an entrance, and it was
       measured at that value. Two staggers, two jobs, neither guessing.

       BOTH VALUES WERE CUT ON 2 SEPTEMBER 2026, from 720ms and 60ms, and the
       reason is arithmetic rather than taste. --ease-out is
       cubic-bezier(0.16, 1, 0.3, 1), which is an aggressive ease-out: solve it
       and 87.5 percent of the travel is done at 29.75 percent of the duration
       and 97.3 percent at 50.6 percent. At 720ms that put the last 356ms of
       every entrance in charge of 2.7 percent of the distance, which on a 14px
       rise is under half a pixel. Nobody could see it, and meanwhile the home
       hero's nine staggered steps did not settle until 540 + 720 = 1260ms.
       Measured in the browser, not computed on paper.

       At 440ms the same curve still delivers 97 percent of the travel by
       222ms, so nothing visible is lost, and at a 40ms stagger the hero settles
       at 360 + 440 = 800ms. A long fade-up sequence on load is the single most
       recognisable template-site tell there is, and the fix was never to remove
       the choreography. It was to stop the choreography outlasting its own
       visible part. If you lengthen these again, solve the curve first.

       LENGTHENED TO 520ms LATER THE SAME DAY, and the curve was solved first as
       that paragraph demands. Nothing above is retracted: the tail really is
       worthless, and at 520ms the last 257ms still carry only 2.7 percent of the
       travel. The reason to lengthen is a different quantity, PEAK VELOCITY,
       which only became a problem once --lift went from 14px to 2rem.

       Differentiate the curve. y(t) = 1 - (1-t)^3 and x(t) = 0.48t(1-t)^2 +
       0.9t^2(1-t) + t^3, so at t=0 the vertical rate is 3 and the horizontal
       rate is 0.48: the animation opens at 6.25 times its own average speed.
       At 14px over 440ms that average is 32px/s and the opening 200px/s, which
       is a gentle rise. At 32px over 440ms it is 455px/s, and 87.5 percent of
       the distance lands in 131ms. A 32px move that is effectively over inside
       131ms does not read as a rise at all. It reads as a snap, and the eye
       cannot name the direction it came from, which is the entire point of an
       entrance.

       520ms puts the opening at 384px/s and 87.5 percent of the travel at
       155ms, which is above the roughly 120ms floor for reading direction of
       travel. The hero's nine steps now settle at 360 + 520 = 880ms, still
       inside a second, so the template-site tell the cut was made to avoid
       has not come back. */
    --dur-entrance: 520ms;
    --stagger:      40ms;

    /* The editions counter belongs in this group rather than with the state
       durations above, because it is triggered by the same observer that runs
       the entrances and it happens once. It could not simply take
       --dur-entrance, though, and the reason is the same arithmetic the
       paragraph above insists on.

       The counter is a reel. Each digit column carries ten faces plus the one it
       lands on, so a column whose digit is d travels 10 + d cells, and at the
       home page's display size a cell is one line box, near enough 61px. The
       three numbers on that page hold digits from 0 to 8, which is 10 to 18
       cells, 610 to 1100 pixels of strip pulled past a window 61px tall.

       Pacing across that distance is --ease-reel's job and the arithmetic is in
       its comment, not here. What this number decides is only the total: long
       enough that the landing has 229ms to decelerate in, short enough that the
       group settles inside a second. The three columns are offset by two
       --stagger steps each, so they settle at 160 + 720 = 880ms, which is the
       figure the home hero's nine steps already land on.

       Deliberately not --dur-slow: that token names the two effects it exists
       for in its own comment, and a third would make the comment wrong. */
    --dur-reel:     720ms;

    /* Travel distances, each a token rather than a literal because the motion
       policy at the end of base.css switches an effect off by setting its
       travel to zero. The rule keeps running and simply moves nothing, which
       is much harder to get wrong than maintaining a parallel set of
       `animation: none` overrides that has to be kept in step by hand.

       THE FIRST TWO WERE RAISED ON 2 SEPTEMBER 2026, from 14px and 0.9rem, and
       this is the only change on the site that came from a measurement of the
       reader rather than of the code. The report was "no animation" on a site
       running twenty-two of them, so the browser was asked what was actually
       happening. Every timeline was live, every feature supported, reduced
       motion off, and not one element stuck invisible. The numbers that mattered
       were these: the viewport was 415px tall, the reveal blocks are 400 to
       500px tall so exactly ONE is ever on screen at a time, and the rise was
       14px. Fourteen pixels against 415 is 3.4 percent of the screen, spent on
       one element with nothing beside it to measure against. The motion was
       real, correct, and below the threshold of being noticed, which for the
       reader is indistinguishable from absent.

       2rem is 32px at the default root size and it is --s-6 exactly, so the
       ambient rise is now one step of the spacing scale rather than a number of
       its own. That is worth more than the pixels: a card rising by the same
       distance that separates it from its neighbour is a relationship, and the
       old 14px was not near anything. */
    --lift:        2rem;    /* ambient rise, one --s-6 step, .reveal's own */
    --lift-line:   130%;    /* a display line, from under its own clip edge */
    --title-wipe:  100%;    /* a section heading, uncovered from its top edge */
    --label-drift: 1.6rem;  /* scroll-driven drift on a mono eyebrow label */
    --plate-drift: 20px;    /* parallax inside an image frame, block axis */
    --wipe:        100%;    /* a contour divider, uncovered from its left edge */
    --shake:       4px;     /* a rejected field, amplitude of one oscillation */

    /* The home hero's terrain drifts sideways as the first screen scrolls away,
       and this is the near layer's full travel; the two behind it take a
       fraction of it, which is what produces the depth. The unit is a px in the
       drawing's own coordinate space, not on the screen, so the effect scales
       with the band instead of getting proportionally larger on a phone. */
    --ridge-drift: 64px;

    /* Two of the animations fade as well as move, and zeroing travel is not
       enough for those: an entrance that starts at `opacity: 0` still shows one
       frame of invisible content even when its duration is 1ms. So the
       entrances are switched by NAME instead, which removes them outright.

       The rule of thumb, and it is the whole reason there are two mechanisms:
       an animation that touches opacity is switched off by name; an animation
       that only moves something is switched off by zeroing its travel. */
    --entrance:      bsd-rise;
    --entrance-line: bsd-line-rise;

    /* Layering */
    --z-base:    0;
    --z-raised:  10;
    --z-sticky:  100;
    --z-header:  200;
    --z-overlay: 300;
    --z-toast:   400;

    /* Colour: LIGHT is canonical.
       Authored in oklch() for perceptual evenness. Two hues carry the design,
       and they are split by JOB rather than by taste, which is the only thing
       that stops a second hue reading as decoration:

         --accent, blue   everything a reader can act on. Links, buttons,
                          hovers, focus rings, form states.
         --brand,  red    the mark's own red, and the small mono markers that
                          label structure rather than offer an action: section
                          numbers, list indices, the edition id, one hairline.

       Nothing is drawn in both. A new component that is clickable is blue; one
       that is a label, an index or a rule is red; one that is neither is ink.
       The one exception is a whole surface: blue also grounds the full-bleed
       accent band and the ridge mark reversed out of it, neither of which is
       clickable. The operative half of the rule is the small half, because a
       chip-sized or label-sized patch of blue is what promises a click, while a
       band-sized one is a ground and promises nothing.
       The rule matters more than the two colours do. Blue used to mean both
       "clickable" and "decorative", which is why the red in the logo had
       nothing on the page to belong to and read as an intrusion. */
    color-scheme: light;

    --paper:        oklch(98.6% 0.0045 85);
    --paper-sunk:   oklch(96.6% 0.006  85);
    --paper-raised: oklch(100%  0      0);

    --ink:          oklch(18%   0.009 265);
    --ink-2:        oklch(42%   0.008 265);

    /* The lightest ink permitted for TEXT, so it is pinned by contrast rather
       than by eye. At 56% it measured 4.49:1 on --paper and 4.23:1 on
       --paper-sunk, both under the 4.5:1 AA floor, and it carries 11px mono
       labels, which are the smallest text on the site. 53% measures 5.05:1 and
       4.76:1, clearing AA on both surfaces with margin while keeping a visible
       step down from --ink-2. Anything lighter than this belongs to a rule, not
       to a word. */
    --ink-3:        oklch(53%   0.007 265);

    --rule:         oklch(89%   0.005 265);
    --rule-strong:  oklch(76%   0.006 265);

    /* Three steps, not two, and --accent-active was added on 2 September 2026
       because an audit found zero :active rules in the entire stylesheet. Every
       button on the site went from rest to hover and then had nothing left to
       say when it was actually pressed, which is the difference between a page
       that photographs well and one that feels alive under a finger.

       The step direction is the rule worth keeping: hover and active both move
       AWAY from the paper, so on the light theme lightness falls 48 to 41 to 35
       and on the dark theme it rises 73 to 81 to 88. That makes the contrast of
       --accent-ink on each step monotonically increasing rather than something
       to re-measure, since the ink is near-white on light and near-black on
       dark and every step retreats from it. A press can never fail AA here by
       construction, which is why there is no measured ratio in this comment. */
    --accent:       oklch(48%   0.215 262);
    --accent-hover: oklch(41%   0.215 262);
    --accent-active:oklch(35%   0.215 262);
    --accent-tint:  oklch(95.5% 0.035 262);
    --accent-ink:   oklch(99%   0.005 262);

    /* The brand red, and the only red on the site. It is the logo's own hue, 29,
       at a chroma and a lightness the rest of the page can live beside.

       The supplied artwork is #FF0000 = oklch(62.8% 0.258 29.2), which is wrong
       twice over. Its chroma is 1.20x the accent blue above, making it the
       loudest colour anywhere on the site, and it measures 3.84:1 on --paper and
       3.62:1 on --paper-sunk, both under the 4.5:1 AA floor, against the blue's
       6.63:1. Loudest and least legible at the same time is exactly the
       combination that pulls an eye off the content a marker exists to
       introduce. #FF0000 is also ON the sRGB boundary at that hue and lightness,
       a ceiling of 0.257 against its 0.258, so the browser clips it and the
       colour measured is not the colour asked for.

       Pinned by measurement on --paper / --paper-sunk / --paper-raised:
         light  50% 0.190  →  6.36 / 6.00 / 6.62   pass
         dark   66% 0.170  →  5.82 / 6.02 / 5.50   pass
       Light lands 0.27 under the accent blue's 6.63:1, so the two read as peers
       with the blue a hair ahead, which is the correct order: action outranks
       label. Measure any change with docs/oklch.py rather than choosing by eye.

       docs/build-logo.py emits the logo's six red paths at these two values, so
       the mark and the markers are the same red by construction rather than by
       somebody remembering. Change this token and rebuild the logo. */
    --brand:        oklch(50%   0.19  29);

    /* Semantic status: only where status is genuinely communicated.
       Always paired with a text label, never colour alone.

       All three carry 11px mono chip text, so all three are pinned by
       measurement on BOTH --paper and --paper-sunk rather than chosen by eye.
       Measured against the 4.5:1 AA floor:
         --status-open   52%  →  4.95 / 4.66   pass
         --status-closed 55%  →  5.07 / 4.77   pass
         --status-warn   62%  →  3.62 / 3.41   FAIL, so it moved to 52%
                         52%  →  5.46 / 5.14   pass
       Amber is the trap here: it looks dark to the eye at a lightness where it
       is nowhere near dark enough for the maths. Warn now sits at the same
       lightness as open, which is also why the three finally look like a set.

       --status-closed sits at hue 29, --brand's, rather than the 25 it was
       authored at. Four degrees is not a difference a reader can see, so two
       reds that close is a distinction only the stylesheet believes in, and the
       next person to read this file would take one of them for a typo. Aligning
       them costs nothing measurable, 5.08 → 5.07 and 4.79 → 4.78, and it makes
       the plain statement true: the site has exactly one red. It keeps its own
       token because it keeps its own job, and that job is never colour on its
       own. It is always attached to the word "Closed", per the top of this
       comment, which is what tells the two uses apart. */
    --status-open:   oklch(52% 0.13 155);
    --status-warn:   oklch(52% 0.15 65);
    --status-closed: oklch(55% 0.17 29);

    /* Derived, so components never re-mix by hand */
    --accent-wash:  color-mix(in oklab, var(--accent) 8%, transparent);
    --ink-wash:     color-mix(in oklab, var(--ink) 5%, transparent);
    --selection-bg: color-mix(in oklab, var(--accent) 20%, transparent);

    /* The brand mark. Two files rather than one currentColor file, because
       currentColor inside an SVG loaded as a CSS background, or through an
       <img>, resolves against that SVG's own root, not the page, so one file
       would render dark in both themes. Verified in a browser, not assumed.
       Switching the URL here means the browser only ever fetches the variant
       actually in use, and the manual theme toggle works for free.
       Generated by docs/build-logo.py; see docs/ARTWORK.md. */
    --logo-url: url("../img/logo-bsides-dehradun-ink.svg");
  }

  /* Colour: DARK. A respectful inversion: ink paper, warm-white ink.
     Applied when the system asks for dark and the user has not forced light,
     or when dark is explicitly selected. Values are intentionally duplicated
     rather than indirected: light-dark() is not available in our Safari floor. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      color-scheme: dark;

      --paper:        oklch(15.5% 0.008 265);
      --paper-sunk:   oklch(12.5% 0.008 265);
      --paper-raised: oklch(19%   0.009 265);

      --ink:          oklch(96%   0.004  85);
      --ink-2:        oklch(78%   0.006 265);
      --ink-3:        oklch(63%   0.007 265);

      --rule:         oklch(28%   0.008 265);
      --rule-strong:  oklch(40%   0.009 265);

      --accent:       oklch(73%   0.155 262);
      --accent-hover: oklch(81%   0.135 262);
      --accent-active:oklch(88%   0.10  262);
      --accent-tint:  oklch(24%   0.055 262);
      --accent-ink:   oklch(14%   0.02  262);

      --brand:        oklch(66%   0.17  29);

      --status-open:   oklch(72% 0.15 155);
      --status-warn:   oklch(78% 0.14 70);
      --status-closed: oklch(70% 0.16 29);

      --shadow-overlay: 0 24px 64px -24px oklch(0% 0 0 / 0.6);

      --logo-url: url("../img/logo-bsides-dehradun-paper.svg");
    }
  }

  :root[data-theme="dark"] {
    color-scheme: dark;

    --paper:        oklch(15.5% 0.008 265);
    --paper-sunk:   oklch(12.5% 0.008 265);
    --paper-raised: oklch(19%   0.009 265);

    --ink:          oklch(96%   0.004  85);
    --ink-2:        oklch(78%   0.006 265);
    --ink-3:        oklch(63%   0.007 265);

    --rule:         oklch(28%   0.008 265);
    --rule-strong:  oklch(40%   0.009 265);

    --accent:       oklch(73%   0.155 262);
    --accent-hover: oklch(81%   0.135 262);
    --accent-active:oklch(88%   0.10  262);
    --accent-tint:  oklch(24%   0.055 262);
    --accent-ink:   oklch(14%   0.02  262);

    --brand:        oklch(66%   0.17  29);

    --status-open:   oklch(72% 0.15 155);
    --status-warn:   oklch(78% 0.14 70);
    --status-closed: oklch(70% 0.16 29);

    --shadow-overlay: 0 24px 64px -24px oklch(0% 0 0 / 0.6);

    --logo-url: url("../img/logo-bsides-dehradun-paper.svg");
  }

  /* Higher contrast where it is cheap to give. */
  @media (prefers-contrast: more) {
    :root {
      --ink-2:       oklch(32% 0.008 265);
      --ink-3:       oklch(42% 0.007 265);
      --rule:        oklch(72% 0.006 265);
      --rule-strong: oklch(52% 0.007 265);
    }
  }

  /* Motion preference. Components must not need to re-declare this, but
     base.css also neutralises transitions globally as a backstop. */
  @media (prefers-reduced-motion: reduce) {
    :root {
      --dur-fast: 1ms;
      --dur:      1ms;
      --dur-slow: 1ms;

      --dur-entrance: 1ms;
      --stagger:      0ms;

      /* Belt and braces rather than the actual switch. The counter's JavaScript
         checks the preference before it builds anything, so under reduced motion
         the reel does not exist and this value is never read. It is here for the
         one case the early return cannot cover: a reader who turns the
         preference on while the reel is mid-spin. */
      --dur-reel:     1ms;

      /* Zeroing the travel matters independently of zeroing the durations, and
         this is the part that is easy to miss: a scroll-driven animation takes
         its progress from a scroll position and never reads a duration at all,
         so 1ms does nothing whatsoever to it. Travel of zero is what actually
         stops it moving. base.css cuts the timeline as well, belt and braces,
         because this preference is the one thing in §5 of the brief that is
         written as non-negotiable. */
      --lift:        0px;
      --lift-line:   0%;
      --label-drift: 0px;
      --plate-drift: 0px;
      --ridge-drift: 0px;
      --wipe:        0%;

      /* --shake has to be zeroed here and cannot rely on the backstop in
         base.css, which is the clearest illustration of why this block exists
         alongside that one. base.css clamps an animation to `duration: 1ms` and
         `iteration-count: 1`; it does not remove it. A three-oscillation shake
         compressed into a single 1ms iteration is still a displacement, so the
         field would jerk once instead of shaking. At zero amplitude every
         keyframe in bsd-field-shake resolves to translateX(0) and the rule runs
         to completion having moved nothing. The rejection is still reported: the
         red border comes from :user-invalid, which is CSS with no motion in it,
         and the browser's own validation bubble is untouched. */
      --shake:       0px;

      /* 0%, not 100%. Every other token here is a distance and zero means "do
         not travel", but --title-wipe is the size of the part that is HIDDEN, so
         zero means "hide nothing" and the heading is simply present. Getting the
         polarity backwards on this one would blank sixty-one headings for
         exactly the readers who asked for less motion. */
      --title-wipe:  0%;

      --entrance:      none;
      --entrance-line: none;
    }
  }
}
