/*
    The live race board: every plate, row and notification, and how each one
    looks and moves -- but not where any of them sits, nor how big.

    That is a layout's job. A layout is one stylesheet in liveraceboard/,
    loaded after this one, which places the parts on the page's grid, sets the
    page's background and scale, and sets the sizes below (--head, --tower,
    --note). Nothing here may name a layout, and a layout should only need to
    say where and how big -- if one needs to restyle a component, the
    component is missing a knob. See liveraceboard/obs.css for the reference.

    Every size is in rem, because the root font size (set by the layout) is
    the only thing that needs to know how big the frame is.
*/

html,
body {
    margin: 0;
    padding: 0;
    /* A board never scrolls; on a broadcast a stray scrollbar would be composited too. */
    overflow: hidden;
}

body {
    font-family: "Segoe UI Semibold", "Segoe UI", "Roboto Condensed", "Arial Narrow", Arial, sans-serif;
    color: #fff;
    /* Nothing here is clickable, and a browser source should not eat clicks. */
    -webkit-user-select: none;
    user-select: none;
}

/*
    The board fills the window. Its parts -- .obs-top (header and bar),
    .obs-tower, .obs-notes, .obs-battles and .obs-foot -- are placed on it
    by the layout, which makes it a grid.

    Every setting is the layout's to set; none has a default here, except
    that the boxes are unframed unless a layout says --framed: yes. The
    sizes (--head, --tower, --note) and --page-pad, the page's padding,
    are read by CSS, and so is --framed (the boxes of notes framed -- see
    below -- running off through that padding). These are
    read by the script, once, at start -- and where one is missing it falls
    back to the plainest board: one plate at a time, unframed:

    on .obs
      --battles-max   how many battles to show at once: only ever the
                      closest, each up for at least 6s and then for as long
                      as it is still among the closest. They stand in
                      .obs-battles, a box of their own; a layout that does
                      not show it has no battles.
    on each box of notes (.obs-notes, and .obs-battles if the layout shows it)
      --max           how many plates it holds. More than one makes it a
                      stack, and the script slides the plates already in it
                      out of the way as one arrives or leaves. Which end is
                      newest is the layout's call -- the script always
                      appends, so column-reverse puts the newest at the top.
      --order         "place": in race order, the battle for the highest
                      place first. Otherwise as they arrive.
      --annex         another box, by name ("battles": .obs-battles), whose
                      room it takes for notes too tall for it -- the race
                      results and Improvements: both frames slide off, and
                      it comes back full height (setTall). "none" for none.
                      Framed layouts only.

    Which kinds a box takes is in the markup (data-takes). .obs-battles takes
    battles, and is display: none here -- a layout that shows it gives
    battles their box; one that does not shows none.
*/
/*
    Fixed, which makes the board a layer of its own (the size of the
    screen: 32MB at 4K). Not absolute to save that: in the page's own layer
    its text gets subpixel antialiasing in a browser window, which a plate
    then loses and regains as it gets a layer to slide and drops it -- the
    snap described on .obs-row. In its own layer, it is greyscale throughout.
*/
.obs {
    position: fixed;
    inset: 0;
    box-sizing: border-box;
    /* Hidden until the script has something real to show. */
    opacity: 0;
    transition: opacity 240ms linear;

    /*
        For real bold. The board's own face, "Segoe UI Semibold", is a family
        of its own on Windows, and asked for 700 it has no bold to give; so
        anything bold sets this at 700 -- the frames' tabs, the fastest lap.
    */
    --bold-face: "Segoe UI", "Roboto Condensed", "Arial Narrow", Arial, sans-serif;

    /*
        A battle plate's height, in --note units: the plate is set to it
        (.obs-note-gap) and the battles' box reserves it (.obs-battles), so
        the two cannot disagree. Left to its content, the plate's height
        depended on how the browser rounded its lines at the window's size:
        12.15 to 12.33 across 176 sizes, against a reserved 12.3, so at 7 of
        them the box grew a fraction of a pixel as the second battle came up
        and shrank as it went (1824 x 1026 among them). 12.4 holds the
        tallest measured. Change the plate and measure again.
    */
    --battle-h: 12.4;

    /*
        Every note is one width, and so are the boxes that hold them, so a
        stack has one straight edge: wide enough for the widest, the battle
        note with two long names -- 46 --note units. Each layout used to set
        it, the same in both, and a least width for plates (--note-min,
        --battle-min) that neither needed once every note had this one.
    */
    --note-w: calc(46rem * var(--note));

    /*
        The fastest-lap purple, defined once. The tower's FL badge and the
        fastest-lap notification are the same fact said twice, so they read it
        from here rather than each carrying a hex -- which is also why the
        notification artwork is drawn in currentColor instead of being handed a
        colour by the script.

        One purple for the badge and the notification's strip, label and art.
        The fastest lap's time in the tower is text on black, where the full
        purple is dark to read, so it takes --purple-lit: halfway to the old
        tint (#cf90f2), which beside the badge read as a different colour --
        light enough to read, still plainly the badge's purple.
    */
    --purple: #901fd1;
    --purple-lit: #b45ce8;

    /*
        The timing system's state colours, on the same terms. The flag strip,
        the progress fill and the notifications all say the same state, and the
        new-leader plate says the same thing as the tower's P1 block -- so each
        is one variable, not a hex copied into every rule that happens to agree
        with it.
    */
    --green: #12c04a;
    --green-lit: #4ade80;
    --red: #e5202e;
    --red-lit: #ff7b82;
    --yellow: #f5c518;
    --yellow-lit: #f3d574;
    --orange: #ff7a00;
    --idle: #3a3a40;

    /*
        Qualifying improvements: the tower's improving strip and the
        Improvements plate. Blue because every other colour here already means
        something, and the strip's first green clashed with the flag and the
        progress bar.
    */
    --blue: #3b9eff;
    --blue-lit: #8cc8ff;

    /*
        The board's whites and greys for text, lightest first, so one used in
        several places is one value.
    */
    --text-bright: #f5f5f7;    /* near white: the chequered flag's strip, REJOINED's block */
    --text-soft: #e8e8ee;
    --text-mid: #c8c8d0;
    --text-out: #b4b4be;       /* an OUT plate's label, 2nd and 3rd's results */
    --text-label: #9a9aa4;     /* the small grey labels */
    --text-dim: #8f8f99;
    --text-faint: #8e8e98;
    --text-dark: #6f6f79;      /* also the OUT plate's strip */

    /* The podium, on the race result. */
    --gold: #e0ac2b;
    --gold-lit: #f2cf6e;

    /* Polished gold, as gradient stops: the winner's medal and the podium's strip. */
    --gold-leaf: #9a6b0f 0%, var(--gold) 28%, #fff1b8 48%, var(--gold) 64%, #a8760f 100%;
    --silver: #c4c8d0;
    --bronze: #c98a52;

    /* Every plate on the overlay -- header, bar, corner panel, notes. */
    --plate: rgba(12, 12, 14, .98);
    --plate-shadow: 0 .3rem 1.2rem rgba(0, 0, 0, .55);

    /*
        A frame round a box of plates, for a layout that frames its boxes
        (.obs-notes, .obs-battles): a fill between the background and the
        plates, an edge, and a tab down its left side carrying the box's name
        (data-label) set sideways. The look is here, so every framed box in
        every layout is the same frame; which boxes wear it, and which edge it
        runs off, is the layout's.
    */
    --frame-fill-base: rgba(8, 8, 10, .45);
    --frame-fill: var(--frame-fill-base);   /* a layout may tint it over the base */
    --frame-edge: rgba(255, 255, 255, .2);
    --frame-edge-w: .2rem;
    --frame-tab: rgba(255, 255, 255, .06);
    --frame-label: var(--text-faint);
    --frame-radius: 2rem;
    --frame-pad: 2.4rem;
    --frame-tab-w: 3.2rem;
    --frame-ms: 1100ms;  /* opening and tucking away; the script waits on it too */

    /* A plate's slide in or out of its box. The script times itself by it. */
    --note-slide: 1200ms;

    /*
        How far a plate slides to be wholly out of sight: its own size (100%,
        along whichever axis the box's --note-from moves it), the page
        padding it stands in from the edge, and room for its shadow. Short
        of that it stopped with an edge still on screen, and then blinked
        out.
    */
    --note-away: calc(100% + var(--page-pad) + 3.5rem);

    /*
        Decelerating into place, for the tower's rows changing places, the
        badges and the progress bar (SETTLE_EASE, which reads it). Quick off
        the mark: it starts at 3.2 times the move's average speed.
    */
    --settle-ease: cubic-bezier(.22, .7, .2, 1);

    /*
        Decelerating into place, for the slides on and off screen: a plate in
        or out of its box, the plates making room, a frame opening or tucking
        away, the tower stacking on and the header coming back (SLIDE_IN_EASE,
        which reads it). Ease-out-quad, which starts at twice the average
        speed rather than --settle-ease's 3.2: a slide's biggest step from one
        frame to the next is its first, and at 60Hz each step is twice what it
        is at 120Hz, so a long slide on --settle-ease read as jitter there.
        The script's slides off (SLIDE_OUT_EASE) are the same curve reversed.
    */
    --slide-ease: cubic-bezier(.5, 1, .89, 1);

    /* Between the plates in a box of them. */
    --stack-gap: 1.2rem;
}

/*
    The plates share one look. Slightly see-through, so a little of the feed
    shows behind them -- which is safe here, unlike the tower's rows: the
    antialiasing problem described on .obs-row only bites something that is
    re-rasterised mid-animation, and these either never move or move whole.
*/
.obs-head,
.obs-foot,
.obs-notice,
.obs-progress,
.obs-note {
    background: var(--plate);
    box-shadow: var(--plate-shadow);
    overflow: hidden;
}

.obs.is-ready {
    opacity: 1;
}

/*
    ?aspect=16:9 (.is-16x9 on the page): the board in the largest 16:9
    frame the window holds, centred, with black bars round it -- how it will
    look on a 16:9 screen, seen from a window of another shape. The scale
    needs nothing: a layout's 1rem is already 0.5% of that frame's width
    (min(.5vw, .8889vh)). The bars are the board's own shadow, so they cover
    the page's background and anything drawn under the board (a layout's
    sheen), and the board is clipped to the frame, as the window's edge
    clips it otherwise: plates slide off its edges, and the fullscreen frames
    run off the right.
*/
.is-16x9 .obs {
    width: min(100vw, 177.7778vh);
    height: min(100vh, 56.25vw);
    margin: auto;
    overflow: clip;
    box-shadow: 0 0 0 100vmax #000;
}

/*
    No race to show -- the meeting has not started, or upstream could not be
    asked. The corner panel still says whose track and what time it is, and a
    plate in the header's place says why the rest is missing. Everything else
    stays out of sight; the first snapshot takes the page to is-ready.
*/
.obs.is-waiting {
    opacity: 1;
}

/* Scoped to .obs so it outranks the plate rule below that makes it flex. */
.obs:not(.is-waiting) .obs-notice,
.obs.is-waiting .obs-top > :not(.obs-notice) {
    display: none;
}

.obs-notice .obs-round:empty {
    display: none;
}

/*
    The header and the bar travel together in .obs-top, so the bar is always
    exactly the header's width: the header sizes the wrapper, and the bar
    stretches to it. justify-self: start is what makes it size to its content
    rather than to its grid column.
*/
.obs-top {
    anchor-name: --board-top;       /* the frame-rate meter places itself by it (.obs-fps) */
    justify-self: start;
    display: flex;
    flex-direction: column;
    gap: .55rem;
}

/* -- the header: what is running, and how long is left ------------------- */

/*
    --head is the header's size (and the corner panel's, and the bar's), set
    by the layout on the same terms as --tower and --note.
*/
.obs-head,
.obs-foot,
.obs-notice {
    display: flex;
    align-items: stretch;
    gap: 0;

    border-radius: calc(.5rem * var(--head));

    /*
        Both plates are pinned to one height rather than left to their content,
        because they sit in opposite corners where nothing else would keep them
        equal -- change what either one contains and they would silently drift
        apart. Sized to clear the tallest content in either.
    */
    height: calc(9rem * var(--head));
}

/*
    The date carries more than the header's one-word label does, and with the
    seconds gone there is room for it, so it is scaled up here rather than in
    the shared rule -- which would also enlarge "Elapsed" up in the header.
*/
.obs-foot {
    anchor-name: --board-foot;      /* for the frame-rate meter (.obs-fps) */
}

.obs-foot .obs-clocklabel {
    font-size: calc(1.5rem * var(--head));
}

.obs-logo {
    display: flex;
    align-items: center;
    justify-content: center;
    /*
        Top, sides, bottom. The badge's own artwork sits slightly low in its
        canvas, so a touch more above than below keeps it looking centred.
    */
    padding: calc(.75rem * var(--head)) calc(.95rem * var(--head)) calc(.6rem * var(--head));
}

/*
    The badge fills the plate's height rather than being given one of its own,
    so it is as large as the panel allows whatever its shape, and it keeps up
    automatically if the plate height changes. The generous max-width is there
    for wide badges -- object-fit then letterboxes rather than distorting.
*/
.obs-logo img {
    display: block;
    height: 100%;
    width: auto;
    max-width: calc(24rem * var(--head));
    object-fit: contain;
}

/*
    No badge on disk: the club's name, set in type, takes the badge's place.
    Russo One is vendored (lib/fonts, OFL) rather than loaded from Google, so a
    broadcast never waits on a third party for it.

    The box is a fixed width so the corner panel does not change size from one
    club to the next; the script (fitClubName) then shrinks the type until the
    name fits it, which covers everything from "GMCC" to "Devonshire Dumplings
    RC Arena". The slant is a skew rather than an italic, which the face does
    not have, and the brushed-metal fill is a gradient clipped to the letters.
*/
@font-face {
    font-family: "Russo One";
    font-style: normal;
    font-weight: 400;
    font-display: block;
    src: url("../lib/fonts/russo-one/russo-one-latin.woff2") format("woff2");
}

.obs-clubname {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: calc(28rem * var(--head));
    /* More on the left, to clear the speed stripes. */
    padding: calc(.9rem * var(--head)) calc(1.5rem * var(--head)) calc(.9rem * var(--head)) calc(4.4rem * var(--head));
    overflow: hidden;
}

/*
    The splash of colour: three slanted speed stripes down the left edge,
    fading inwards, and a tint in the lettering. --club is set on the element
    by the page: one of six colours, chosen server-side from a hash of the
    club's name (ClubColour), so it is the same every night. Decoration only --
    it says nothing about the race, so it keeps clear of the exact flag and
    fastest-lap shades rather than borrowing the variables.

    The stripes are an SVG mask filled with the club's colour, written once
    as --stripes for both mask properties. SVG because its edges are
    anti-aliased properly: hard-stopped gradients under a skew came out
    stepped, and clip-path was
    better but still a little rough. The drawing is in the plate's own units
    (9 tall, the plate being 9rem * --head), each stripe leaning 1.425 across
    its height -- tan(9deg), the lettering's slant.
*/
.obs-clubname::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    /*
        Pulled left so the first stripe's outer edge is tangent to the plate's
        rounded bottom-left corner (radius .5, centre at (.5, 8.5) in these
        units): the curve trims the stripe's foot and runs straight into it.
        Worked out, not eyeballed -- move the stripes or the radius and redo it.
    */
    left: calc(-.285rem * var(--head));
    width: calc(4.2rem * var(--head));
    background: var(--club);
    --stripes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4.2 9' preserveAspectRatio='none'%3E%3Cpolygon points='1.625,0 2.245,0 .82,9 .2,9'/%3E%3Cpolygon fill-opacity='.62' points='2.545,0 3.165,0 1.74,9 1.12,9'/%3E%3Cpolygon fill-opacity='.3' points='3.465,0 4.085,0 2.66,9 2.04,9'/%3E%3C/svg%3E");
    -webkit-mask: var(--stripes) no-repeat 0 0 / 100% 100%;
    mask: var(--stripes) no-repeat 0 0 / 100% 100%;
}

#obsClubName {
    display: block;
    max-width: 100%;
    font-family: "Russo One", "Segoe UI Black", "Arial Black", sans-serif;
    font-size: calc(3rem * var(--head));
    line-height: .95;
    letter-spacing: .015em;
    text-transform: uppercase;
    text-align: center;
    /* Words are never broken: a long one overflows, and fitClubName shrinks the type until it does not. */
    text-wrap: balance;
    /*
        The translate centres the capitals rather than the line. Russo One's
        line box keeps room for descenders (ascent .926em, descent .279em), and
        in upper case nothing uses it, so the caps (.719em tall) sat .028em
        above centre -- 2px at this size, measured. text-box: trim-both cap
        alphabetic is the proper fix, but it needs Chrome 133 and OBS's
        browser is older.
    */
    transform: translateY(.028em) skewX(-9deg);
    color: #fff;
    filter: drop-shadow(0 .12rem 0 rgba(0, 0, 0, .6));
}

/*
    Airbrushed chrome with the club's colour worked in, strongest below the
    horizon. It is on an inline span with box-decoration-break: clone, so each
    line gets its own copy -- painted once over the block, the first of two
    lines was all sky and the second all ground.

    An inline box's background covers its content area (ascent .926em plus
    descent .279em), not the line, so the stops are placed on the capitals
    inside it: their tops at 17%, their middle -- the horizon -- at 47%, the
    baseline at 77%.
*/
#obsClubName > span {
    background: linear-gradient(180deg,
        #ffffff 17%,
        color-mix(in srgb, var(--club) 18%, #e9ebf0) 45%,
        color-mix(in srgb, var(--club) 50%, #aeb3bf) 49%,
        color-mix(in srgb, var(--club) 35%, #d6d9e0) 77%);
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/*
    The timing system's own colour code, mirrored down the leading edge: green
    while a race runs, red and yellow around it, black when nothing is on. The
    class is chosen server-side from an allowlist -- see LiveRaceParser.
*/
.obs-flag {
    width: calc(.9rem * var(--head));
    background: var(--idle);
    transition: background-color 300ms linear;
}

.obs-green  { background: var(--green); }
.obs-red    { background: var(--red); }
.obs-yellow { background: var(--yellow); }
.obs-orange { background: var(--orange); }
.obs-black,
.obs-unknown { background: var(--idle); }

.obs-headtext {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: calc(.25rem * var(--head));
    padding: calc(1rem * var(--head)) calc(1.6rem * var(--head));
}

/*
    The split between the two halves of a plate. Written as "whatever follows
    the first block" so it stays correct with the clock now leading the header,
    and so the corner panel has no stray edge when a club has no badge.
*/
.obs-clockbox + .obs-headtext,
.obs-logo + .obs-clockbox,
.obs-clubname + .obs-clockbox {
    border-left: .1rem solid rgba(255, 255, 255, .14);
}

/*
    The meeting the race belongs to. Smallest and dimmest of the three lines,
    and the first, so the header reads downwards from the least specific to the
    most: which event, which round of it, which race. It is the line you need
    least often, which is why it is the one that gets least of the plate.

    Hidden outright when empty rather than left as a blank line, or the flex
    gap above the round would still be there for a meeting with no name.
*/
.obs-event {
    font-size: calc(.95rem * var(--head));
    font-weight: 600;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-faint);
    white-space: nowrap;
}

.obs-event:empty {
    display: none;
}

.obs-round {
    font-size: calc(1.15rem * var(--head));
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: #b6b6c0;
    white-space: nowrap;
}

.obs-race {
    font-size: calc(2rem * var(--head));
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    white-space: nowrap;
}

.obs-clockbox {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: calc(1rem * var(--head)) calc(1.8rem * var(--head));
    min-width: calc(11rem * var(--head));
}

/*
    The header's label changes with the state -- "ELAPSED" through a race,
    "UNTIL RACE START" before one -- and the longer one is wider than the
    digits above it. Without a width reserved for the worst case the whole
    plate, its divider and the round and race beside it would jump every time
    a race starts or ends. Scoped to the header so the corner panel, whose
    label is a date that never changes length, keeps its own size.
*/
.obs-head .obs-clockbox {
    min-width: calc(18rem * var(--head));
}

/*
    The bold figures -- both clocks, the tower's places and laps -- are in
    real bold, --bold-face, which is what makes their digits all one width.
    In the board's own face, "Segoe UI Semibold", the 7 is narrower than
    the other digits whatever the weight, tabular figures asked for or not
    (53.63 against 55.53 at 100px); asked for 700 it has no bold, so the
    browser made one up over the same glyphs. The corner panel sizes to the
    time, so it changed width as a 7 came or went -- seen at 15:47 turning
    15:48 -- and the tower's laps changed width with their digits. Segoe UI
    Bold's digits are all 57.53. The times, at 600, are still Semibold, 7
    and all: no semibold has tabular figures to give, and the columns are
    sized for the widest digit (setColumnWidths), so a 7 moves nothing.
*/
.obs-clock,
.obs-pos,
.obs-laps,
.obs-noteplace,
.obs-fltext,
.obs-row.shows-best .obs-pred.is-fastest .obs-time,
.obs-stat.is-fastest {
    font-family: var(--bold-face);
    font-weight: 700;
}

.obs-clock {
    font-size: calc(5.2rem * var(--head));
    line-height: .95;
    /* Digits must not jitter as the clock ticks. */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/*
    Each character of the corner clock is its own cell (showClock), so a changing
    digit can flick on its own the way the tower's figures do. Inline blocks,
    because an inline box cannot be moved with a transform.
*/
.obs-digit {
    display: inline-block;
}

.obs-clocklabel {
    font-size: calc(.95rem * var(--head));
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--text-label);

    /*
        The plate is a fixed height with overflow hidden, so a label that wraps
        is not accommodated, it is cut in half. Same rule as .obs-round and
        .obs-race above, and it is the width reserved on .obs-head .obs-clockbox
        that keeps the longest one on its line.
    */
    white-space: nowrap;
}

/* -- the progress bar: how far through its time the race is ------------- */

/*
    A slim plate under the header, on the header's own terms (--head, the same
    background, radius and shadow), so it reads as part of it rather than as a
    second thing.

    Both ends are labelled because the end is the point: 0:00 on the left, the
    race's length on the right, either side of the track. That is what says
    where 100% is -- a bar that simply fills tells you "some of the way" and
    nothing about how long is left.
*/
.obs-progress {
    display: flex;
    align-items: center;
    gap: calc(.9rem * var(--head));
    padding: 0 calc(1.1rem * var(--head));
    height: calc(2.6rem * var(--head));
    border-radius: calc(.5rem * var(--head));
    transition: opacity 500ms ease-in-out;
}

/*
    Counting down to a race, or between them: the clock is a countdown and
    there is no progress to show, so the bar fades away until the race starts.
*/
.obs-progress.is-idle {
    opacity: 0;
}

/*
    No length to measure against, so nothing honest to draw. Hidden but still
    holding its space, which costs nothing over a transparent frame -- and
    means nothing moves if a race's length comes and goes.
*/
.obs-progress.is-none {
    visibility: hidden;
}

.obs-progend {
    font-size: calc(1.05rem * var(--head));
    font-weight: 700;
    letter-spacing: .04em;
    color: var(--text-label);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";

    /* The race's length can be 5:00 or 10:00; one width for both. */
    min-width: 2.6em;
}

.obs-progend-to {
    text-align: right;
    color: var(--text-bright);
}

/*
    The groove. The fill, the ticks and the flag all sit inside it and are
    positioned against it, so the track's width is 100% of the race's time
    and nothing else.

    The groove runs the full length of the race, so its own ends are the start
    and the finish, with the times beside them saying which is which.
*/
.obs-progtrack {
    position: relative;
    flex: 1 1 auto;
    height: calc(.8rem * var(--head));
    background: rgba(255, 255, 255, .09);

    /* Fully rounded ends, and the fill below rounds to match. */
    border-radius: 999px;
    overflow: hidden;
}

/*
    Minute marks, built by the script once per length. Behind the fill, so the
    time already run is solid and the marks count down what is left.
*/
.obs-progticks {
    position: absolute;
    inset: 0;
}

.obs-progtick {
    position: absolute;
    top: 22%;
    bottom: 22%;
    width: calc(.12rem * var(--head));
    margin-left: calc(-.06rem * var(--head));
    background: rgba(255, 255, 255, .5);
}

/*
    The full width of the track, slid in from the left: translateX(-100%) is
    empty and none is full, with the track's rounded overflow clipping what is
    still off to the left. Slid rather than scaled, because a scaleX squashes
    the rounded leading end into a flat sliver; and slid rather than resized,
    because the glide runs the whole race -- a width transition would lay out
    and repaint every frame of it, a transform stays on the compositor. Its
    transition is set by the script: linear over the second between two clock
    ticks, so it glides at the race's own pace rather than stepping.

    Its colour follows the state bar -- the same colour as the strip on the
    header's leading edge, because it is the same race in the same state.
*/
.obs-progfill {
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    border-radius: inherit;
    background-color: var(--green);

    /* A little light along the top edge, so it reads as a bar, not a stripe. */
    box-shadow: inset 0 calc(.12rem * var(--head)) 0 rgba(255, 255, 255, .28);

    /* Clips the ticks it has passed to itself (below). */
    overflow: hidden;
}

/*
    The minute marks the fill has passed, still to be seen: a darker set
    drawn inside the fill, so they show only where it has covered. The fill
    clips them to itself (and to its rounded end); the script slides them
    back the way the fill came by the same amount (fillTo), so they stay on
    the marks. Darkened from whatever the fill's colour is, so they suit
    red and amber too.
*/
.obs-progticks-done .obs-progtick {
    background: rgba(0, 0, 0, .35);
}

.obs-progress.is-red    .obs-progfill { background: var(--red); }
.obs-progress.is-yellow .obs-progfill { background: var(--yellow); }
.obs-progress.is-orange .obs-progfill { background: var(--orange); }

/* -- notifications -------------------------------------------------------- */

/*
    Where the notes sit, and how many may be up at once (a box's --max),
    is the layout's. So is where a note comes from: the layout sets
    --note-from on each box, its plates' off-screen transform -- usually
    translateX or translateY of --note-away -- and a plate slides from
    there to its place and back.
    No fade: moving and dissolving at the same time reads as two things
    happening at once. The transition's length is --note-slide, which the
    script reads to know when a plate has landed or gone (NOTE_SLIDE_MS), so
    it lives here rather than in a layout.

    --note is the note's size, set by the layout on the same terms as --head
    and --tower.
*/
.obs-notes {
    display: flex;
    flex-direction: column;
    gap: var(--stack-gap);
    width: var(--note-w);
    pointer-events: none;
}

/*
    The battles' own box. Hidden unless the layout shows it (display: flex,
    and a place), and then it keeps room for its --max battle plates whether
    or not they are up, so nothing moves as they come and go: --battle-h,
    which the plates are set to (on .obs). The height is the plates' alone, hence content-box: padding
    counted inside it came out of the plates' room, and the box grew as the
    last one arrived, popping the one above it up.
*/
.obs-battles {
    display: none;
    flex-direction: column;
    gap: var(--stack-gap);
    width: var(--note-w);
    box-sizing: content-box;
    min-height: calc(var(--max) * var(--battle-h) * 1rem * var(--note) + (var(--max) - 1) * var(--stack-gap));
    pointer-events: none;
}

/*
    Framed boxes, for a layout that sets --framed: yes on .obs. Each box of
    plates sits in a frame -- a fill between the background and the plates,
    an edge, and a tab down its left side carrying the box's name
    (data-label) set sideways -- that runs off the right-hand edge of the
    screen through the page's padding (--page-pad), so only its left corners
    are rounded. The look is the --frame-* tokens above.

    An empty box tucks itself away into that edge, leaving its tab and a
    sliver of frame showing, and comes back out as its first plate arrives.
    :empty is exact here: a leaving plate is still in the box until its exit
    is over, so the frame only goes once the last plate has. The script
    waits on it (--frame-ms), and keeps a tucked box shut a while.

    A style query, so nothing here applies unless a layout asks for it.
*/

/* A length, so the script reads it in px (snapFrames). */
@property --frame-tuck {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

@container style(--framed: yes) {
    .obs-notes,
    .obs-battles {
        position: relative;
        box-sizing: content-box;
        margin-right: calc(-1 * var(--page-pad));
        padding: var(--frame-pad) var(--page-pad) var(--frame-pad) calc(var(--frame-tab-w) + var(--frame-pad));
        background: var(--frame-fill);
        border: var(--frame-edge-w) solid var(--frame-edge);
        border-right: 0;
        border-radius: var(--frame-radius) 0 0 var(--frame-radius);
        transition: transform var(--frame-ms) var(--slide-ease);

        /*
            Still, open or tucked, on whole screen pixels. A layer that is
            moving is drawn at a fraction of a pixel by resampling it, a
            still one by drawing its text again at that fraction, so a frame
            at rest off the pixel grid looked different the moment it moved
            and again as it landed: a pop on the tab's label. The script
            (snapFrames) measures where each box is laid out and nudges it
            onto the grid by --snap-x / --snap-y, and sets --tuck-x, the
            tucked offset below, rounded the same way.

            Promoted for good, as .obs-row is: drawn once, at that position,
            and only ever moved.
        */
        transform: translate(var(--snap-x, 0px), var(--snap-y, 0px));
        will-change: transform;

        /*
            Tucked, what is left on screen is the edge, the tab with the
            box's name, and --frame-peek of the frame itself: .3rem, a
            quarter of the 1.2rem it was, so the name is all that really
            shows. --frame-tuck is all of that, which the script reads (in
            px: a registered length) to round the tucked offset.
        */
        --frame-peek: .3rem;
        --frame-tuck: calc(var(--page-pad) + var(--frame-edge-w) + var(--frame-tab-w) + var(--frame-peek));
    }

    /* .is-held: emptied with the next plate already waiting (exitNotes). */
    .obs-notes:empty:not(.is-held),
    .obs-battles:empty:not(.is-held) {
        transform: translate(var(--tuck-x, calc(100% - var(--frame-tuck))), var(--snap-y, 0px));
    }

    /*
        .is-gone: right off the screen, tab and all, while the notes box
        changes height (growNotes). The box runs to the screen's edge, so
        its own width takes it off; a rem more for good measure. After the
        tucked rule, and as specific, so it wins over it.
    */
    .obs .obs-notes.is-gone,
    .obs .obs-battles.is-gone {
        transform: translate(calc(100% + 1rem), var(--snap-y, 0px));
    }

    /*
        The tab down the left side, carrying the box's name: the strip and
        its divider here, the name on ::after.

        Both under the plates (z-index -1, inside the frame, which is a
        stacking context of its own: will-change). Painted after them, as
        they were, the name was lifted out of the frame's layer into one of
        its own whenever a plate slid -- the browser assumes a moving layer
        overlaps whatever paints after it -- and drawn differently there:
        the label went jagged as a plate set off and smooth as it landed.
    */
    .obs-notes::before,
    .obs-battles::before,
    .obs-notes::after,
    .obs-battles::after {
        z-index: -1;
    }

    .obs-notes::before,
    .obs-battles::before {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        width: var(--frame-tab-w);
        background: var(--frame-tab);
        border-right: .1rem solid var(--frame-edge);
        border-radius: var(--frame-radius) 0 0 var(--frame-radius);
    }

    /*
        The box's name, reading upwards in the middle of the tab (between
        the frame's edge and the divider): a line of horizontal text, turned
        a quarter round about its own middle. Horizontal, not writing-mode
        vertical text, because that is how the script measures it.

        Centred on the word's ink, not its line box:
        - Across: a line box has room above the capitals that it does not
          have below, by an amount that depends on the face and the browser
          (Chrome on iOS differs from Safari). The script measures it
          (centreFrameLabels) and moves the word by --label-dy, before the
          turn.
        - Along: letter-spacing puts its space after the last letter too,
          so the same on the left gives it back.
    */
    .obs-notes::after,
    .obs-battles::after {
        content: attr(data-label);
        position: absolute;
        top: 50%;
        left: calc(var(--frame-tab-w) / 2);
        padding-left: .35em;
        transform: translate(-50%, -50%) rotate(-90deg) translateY(var(--label-dy, 0em));
        color: var(--frame-label);
        pointer-events: none;
    }

    /*
        The name's lettering, shared with the hidden copy the script lays
        out to measure it (.obs-labelprobe). Real bold: --bold-face, on .obs.
    */
    .obs-notes::after,
    .obs-battles::after,
    .obs-labelprobe {
        white-space: nowrap;
        line-height: normal;
        font-family: var(--bold-face);
        font-size: 1.69rem;
        font-weight: 700;
        letter-spacing: .35em;
        text-transform: uppercase;
    }
}

/*
    Never shrunk to fit. A plate is overflow: hidden, which takes away a
    flex item's own least height, so in a stack that did not fit (the
    fullscreen notes box is a flex column) the plates quietly squashed and
    clipped their contents. Now a stack that does not fit overflows its box
    where it can be seen. How much fits is measured -- see --note in
    fullscreen.css.
*/
.obs-note {
    /*
        A kind of note's colours, set once per kind below: its strip, its
        label and its art (drawn in currentColor). By default the idle strip,
        a grey label and art in the plate's own colour.
    */
    --note-strip: var(--idle);
    --note-label: var(--text-label);
    --note-art: currentColor;

    display: flex;
    flex-shrink: 0;
    box-sizing: border-box;
    width: var(--note-w);
    align-items: stretch;
    border-radius: calc(.5rem * var(--note));
    transform: var(--note-from);   /* the box's; none outside a box */
    transition: transform var(--note-slide) var(--slide-ease);
}

.obs-note.is-in {
    transform: none;
}

/* The strip down the leading edge -- the same idea as .obs-flag on the header. */
.obs-notestrip {
    width: calc(.9rem * var(--note));
    background: var(--note-strip);
}

/*
    One width for every plate's picture, whatever is drawn in it: the 3.6rem
    art and 1.15rem either side. Sized to its art, the podium's bigger
    chequered flag made its column 2.8rem wider than the rest, which showed
    wherever plates stack (fullscreen): the divider and the text stepped out.
*/
.obs-noteart {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: calc(5.9rem * var(--note));
    padding: calc(.85rem * var(--note)) 0;
    color: var(--note-art);
}

.obs-noteart svg {
    display: block;
    width: calc(3.6rem * var(--note));
    height: calc(3.6rem * var(--note));
}

/* Same divider as the header's, and for the same reason. */
.obs-notetext {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: calc(.12rem * var(--note));
    padding: calc(.9rem * var(--note)) calc(1.7rem * var(--note));
    border-left: .1rem solid rgba(255, 255, 255, .14);
    min-width: calc(15rem * var(--note));
}

.obs-notelabel {
    font-size: calc(1.235rem * var(--note));
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--note-label);
    white-space: nowrap;
}

/*
    The part of a label worth reading -- the place in "Battle for 3rd".
    Thickened with a stroke rather than a weight, for the reason given on
    .obs-gapvalue: the one face in the stack renders every weight the same.
*/
.obs-notelabel-em {
    font-weight: 900;
    -webkit-text-stroke: .05em currentColor;
    paint-order: stroke fill;
}

/*
    "3rd": the suffix set smaller than the number, on the same baseline, so
    the number is what the eye lands on. The label's letter-spacing and the
    stroke above are em lengths, which a child inherits already worked out at
    the label's size, so both are given again here to scale with the
    suffix: otherwise its letters spread out and thicken.
*/
.obs-ordinal {
    font-size: .72em;
    letter-spacing: .2em;
    -webkit-text-stroke-width: .05em;
}

.obs-notemain {
    display: flex;
    align-items: center;
    font-size: calc(2rem * var(--note));
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    white-space: nowrap;
}

/*
    The leader a new leader replaced (buildStandardNote), under the
    headline: the old name small and dimmed, behind a grey down arrow for
    "dropped", quiet. Under, not over: whatever is read first reads as the
    leader.
*/
.obs-notebefore {
    --caps: .755em;        /* the name's capitals, measured in this face (Segoe UI Semibold) */
    display: flex;
    align-items: baseline;
    gap: .4em;
    font-size: calc(1.15rem * var(--note));
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-dim);
    white-space: nowrap;
}

/*
    The arrow, --arrow-h tall (the size the battle plate's gap arrow was, when
    it had one), its middle level with the middle of the name's capitals:
    on the name's baseline (a flex item with no text takes its bottom edge
    as its baseline), then moved by half the difference between its height
    and theirs. Centred on the line box instead, it sat off the capitals.
*/
.obs-notedrop {
    --arrow-h: calc(1.05rem * var(--note));
    display: block;
    position: relative;
    flex: none;
    width: calc(var(--arrow-h) * 1.2);   /* the arrow's 12:10 */
    height: var(--arrow-h);
    top: calc((var(--arrow-h) - var(--caps)) / 2);
    color: var(--text-dark);
}

.obs-notedrop svg {
    display: block;
    width: 100%;
    height: 100%;
    transform: rotate(180deg);
}

.obs-notedetail {
    font-size: calc(1.15rem * var(--note));
    font-weight: 600;
    letter-spacing: .06em;
    color: var(--text-mid);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/*
    On the fastest lap the time is the news -- the name says who, but the
    number is what the plate exists to show -- so it is not left at the size
    of a footnote. Near the headline's size, and white rather than grey.
*/
.obs-note-fastest .obs-notedetail {
    font-size: calc(1.7rem * var(--note));
    font-weight: 700;
    -webkit-text-stroke: .03em currentColor;
    paint-order: stroke fill;
    color: #fff;
}

/*
    A change in a time, told the same way wherever it appears: how much
    quicker the new fastest lap is than the one it replaced (.obs-notegain,
    after the time), and how much a battle's gap last moved (.obs-gapdelta,
    under it). One rule, so the two cannot drift apart -- they had: the
    battle's was bigger, in real bold, unstroked, and in the flags' full
    colours.

    Every size is absolute, in the units the fastest lap's time uses (1.7rem,
    .obs-note-fastest .obs-notedetail): the gain inherits its letter-spacing
    and stroke from that time, as lengths already worked out at the time's
    size, and the battle's change has no such time to inherit from. The
    board's own face at 700, thickened with the stroke, as the time is.
    Green for better, red for worse: the lit tints, as the small coloured
    labels use, so the red matches the green. (The battle's red was the
    primary --red for a while; it read heavier than the green beside it.)

    The battle's is a third bigger (--gain-scale on .obs-gapdelta), asked
    for: it is the plate's second figure, under the gap, where the fastest
    lap's sits after a time of its own size. Everything scales with it.
*/
.obs-notegain,
.obs-gapdelta,
.obs-impgain {
    --gain-scale: 1;
    font-size: calc(var(--gain-scale) * .72 * 1.7rem * var(--note));
    font-weight: 700;
    letter-spacing: calc(var(--gain-scale) * .06 * 1.7rem * var(--note));
    -webkit-text-stroke: calc(var(--gain-scale) * .03 * 1.7rem * var(--note)) currentColor;
    paint-order: stroke fill;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/* After the time, on its baseline, so the two read as one line. */
.obs-notegain {
    margin-left: .55em;
    color: var(--green-lit);
}

/*
    Equalled, not beaten: "0.00", in yellow (fastestNote's tie). The full
    --yellow, not the -lit tint, as asked for: it is bright enough to read
    on the plate (about 12:1), which is what the tint is there for.
*/
.obs-notegain.is-tie {
    color: var(--yellow);
}

/*
    Per kind: the strip, and the label tinted to match it. Same rule as the
    tower's and the flag's -- the colour is the thing that says which of these
    you are looking at, so it appears twice rather than once.

    The tints are lighter than the strips because they carry small text on
    near-black, where the strip colours themselves go muddy.
*/
/*
    The closest-battle note, which is built differently from the other four: no
    art slot, because the picture here is the road between the two names. The
    divider goes with the art slot it was dividing.

    It reads the way the cars are running, left to right: the driver behind,
    how far down they are, then the driver ahead.
*/
.obs-note-gap {
    height: calc(var(--battle-h) * 1rem * var(--note));   /* fixed: see --battle-h */
}

.obs-note-gap .obs-notetext {
    /*
        Fills the plate, so a plate wider than its names -- every note is
        one width, --note-w -- gets its names at the two ends and the road
        between them taking up the difference. Left its own width it would
        leave a hole on the right.
    */
    flex-grow: 1;
    border-left: 0;

    /*
        Less than the usual 1.1rem, top and bottom alike, since the gap's
        change was made a third bigger: the plate stays --battle-h tall and
        its extra height comes out of the two margins in equal halves.
        The bottom is that margin plus the change's own room under the gap.
    */
    padding-top: calc(1.015rem * var(--note));
    padding-bottom: calc(3.075rem * var(--note));
}

.obs-gaprow {
    display: flex;
    align-items: center;
    gap: calc(.7rem * var(--note));
    margin-top: calc(.25rem * var(--note));
}

/*
    Both cars driving right, the way the race is going, so the one on the left
    is the one doing the chasing. Small: it marks which end is which, it is not
    the subject of the notification.
*/
/*
    A grey halfway (to the eye: mixed in oklab) between the unlit chevrons'
    (--road) and the names' white, so the cars stand out from the road but
    still sit back behind the names and the gap. They were the road's own
    grey, which sank into the plate.
*/
.obs-gapcar {
    display: flex;
    align-items: center;
    color: color-mix(in oklab, var(--road), #fff);
}

.obs-gapcar svg {
    display: block;
    height: calc(1.7rem * var(--note));
    width: auto;
}

/*
    The GT car (BATTLE_CAR in the script): its file -- the 4x PNG or the
    SVG, set as --gt-car -- is a mask, and the colour is this box's own, so
    it is the yellow above like the classic car's currentColor. A little
    taller than the classic car, since a long GT body and its wing need it
    for the body to be about the same size (1.8 --note units against 1.7;
    it was 1.95, and asked to be a little smaller); shorter than the gap
    figure, so the row's height does not change. The aspect is the drawing's
    (the SVG's viewBox, 352.64 x 100).
*/
.obs-gapcar-gt {
    display: block;
    height: calc(1.8rem * var(--note));
    aspect-ratio: 352.64 / 100;
    background: currentColor;
    -webkit-mask: var(--gt-car) 0 0 / 100% 100% no-repeat;
            mask: var(--gt-car) 0 0 / 100% 100% no-repeat;
}

/*
    Each name sits over its own car, and each gets exactly half the plate.

    Two equal grid columns rather than one row pushed apart, so the centre line
    always falls in the space between the names, which is where the gap value
    is. Both rows are children of the same flex column, so the track below
    stretches to the same width and a name is always above its own car.

    The plate is one fixed width (--note-w), so the gap rolling or an
    overtake cannot make it grow or shrink. The road either side of the gap
    is what gives way when the number gets longer -- hence its small
    minimum below.
*/
.obs-gapnames {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);   /* halves, however long a name (see "name boxes") */
    column-gap: calc(2.4rem * var(--note));

    /* Clear of the label: the boxes stand taller than a line of caps. */
    margin-top: calc(.55rem * var(--note));
}

/*
    Shrink-wrapped to its contents and pushed to its own end of the column,
    so the name's own box is exactly where its text is -- which is what lets
    an overtake slide each name and box from where it really was.
*/
.obs-gapname {
    display: inline-flex;
    align-items: center;
    gap: .45em;
    font-size: calc(1.45rem * var(--note));
    font-weight: 700;
    letter-spacing: .03em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Each one over the end its car is at, with its box on the outside end. */
.obs-gapname:first-child { justify-self: start; }
.obs-gapname:last-child  { justify-self: end; flex-direction: row-reverse; }

/*
    The position, in a box -- the same idea as the tower's FL badge: a solid
    square with the number knocked out of it, in the note's own yellow.

    A fixed square sized for "10", the widest a position gets here, so every
    box is the same size whatever is in it and a 9 becoming a 10 moves
    nothing. Thickened with a stroke for the reason given on .obs-gapvalue.
    line-height 1 plus the bottom padding centres the digits' ink rather than
    the line box, which sits it low -- see .obs-fl; the amount is measured.

    The podium's medal boxes (.obs-podpos) are the same construction, so they
    share this rule rather than a copy of it -- the measured centring above
    is theirs too.
*/
.obs-gappos,
.obs-podpos {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: 1.5em;
    height: 1.5em;
    padding-bottom: .1em;
    border-radius: calc(.15rem * var(--note));
    color: #17171c;
    font-weight: 900;
    line-height: 1;
    -webkit-text-stroke: .03em currentColor;
    paint-order: stroke fill;
    /*
        Proportional digits, and no letter-spacing: the box is a fixed square,
        so tabular widths buy nothing, and they cost the centring. Segoe UI's
        tabular 1 sits in a wide cell with most of its space on the left, and
        negative letter-spacing trims space after the last digit, so "10" sat
        about 3% of the box right of centre (measured, 120px render); with
        both gone it is centred.
    */
    font-variant-numeric: proportional-nums;
}

.obs-gappos {
    background: var(--yellow);
    flex: none;            /* the name gives way, never the box (see "name boxes") */
}

/*
    Name boxes: the boxes a driver's name sits in stop at the plate's edge
    instead of growing with a name too long for it -- a battle's name keeps
    to its half, the podium's names to theirs -- so a name too long for its
    box overflows it, and fitNames makes just that name smaller to fit (any
    name marked .obs-fit). With a name that fits, none of this changes a
    thing: the boxes are as they were.
*/
.obs-gapname {
    max-width: 100%;
}

.obs-gapwho {
    min-width: 0;
}

/*
    The road between them: a row of chevrons pointing the way the cars are
    going, standing still in grey, filling with light from one end (which
    way: below) -- the two roads together. It replaced a grey line.

    Every battle's lights run on one clock: once a plate is on the page its
    lights are started from the document timeline's zero (an animationstart
    listener in the script), so two battles on screen fill in step,
    whenever each came up.

    The chevrons are the road's own mask (so they stand still and only the
    light behind them moves), a whole number of them, so each road starts
    and ends on a whole chevron: the script counts how many --chev-p
    spacings fit the road, to the nearest (fitRoad), and the tiles share
    the road out exactly (--chev-w) -- counted there rather than by
    mask-repeat: round so that the light knows the tiles too. Mostly only
    the gaps between them give, since the drawing is narrower than its
    tile; on a road too short to space out (under two tiles or so) the
    drawing shrinks a touch. One mask layer, nothing composited: a second
    (a gradient fading the ends) left a hairline along the road's top on
    iOS. The light moves by transform and fades by opacity, so it costs no
    repaint per frame. The road keeps the line's flex sizing and least
    width -- it is what gives way when the figure gets longer -- and is
    shorter than the car and the figure, so the row and the plate are the
    size they were.

    The light's colour is the gap's last change, as the change figure under
    the gap tells it: red when it grew, green when it closed, and no light
    at all when there is nothing to tell -- a new plate, or just after the
    two swapped places. (That was the battle's yellow; asked to go: only a
    change is worth the movement.) The figure's own colours, --red-lit and
    --green-lit, so the light and the figure read as one thing (they were
    the primaries, darker than the figure). The script sets it on each road
    (.is-up / .is-down, pointRoads) while the road is faded out at the end
    of a fill, so a light never comes on, goes off or changes colour in
    view, and a fill is never cut short.

    The same change points the roads: with nothing to tell, both run right,
    the way the cars go; closing, they run in towards the figure; growing,
    out from it. A road turns by mirroring it whole (.is-turned: scaleX(-1),
    about its middle), mask and light together, so its fill is the same
    animation on the same clock and every plate's lights stay in step --
    mirrored, the two fill from the outer ends, or the figure, at once.
    The script sets the class (pointRoads) at the end of a fill (turnRoad),
    fading the road out and back in around it only when it turns: a light
    alone changes there unseen, the fill being dark.
*/
.obs-note-gap             { --road: #4d4d56; }
.obs-gapline.is-up        { --light: var(--red-lit); }
.obs-gapline.is-down      { --light: var(--green-lit); }

.obs-gapline {
    --chev-h: calc(1.45rem * var(--note));
    --chev-p: calc(1.8rem * var(--note));          /* one chevron's spacing */
    --chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpolygon points='1,0 6.4,0 11.4,6 6.4,12 1,12 6,6'/%3E%3C/svg%3E");
    position: relative;
    flex: 1 1 auto;
    min-width: calc(.6rem * var(--note));
    --chev-count: 6;          /* the chevrons that fit, and the fill's steps (fitRoad) */
    --chev-cycle: 1.65s;      /* one fill, start to faded; the script reads it (turnRoad, showGap) */
    --chev-w: calc(100% / var(--chev-count));   /* one chevron's tile: the road shared out */
    height: var(--chev-h);
    overflow: hidden;
    -webkit-mask: var(--chev) 0 50% / var(--chev-w) var(--chev-h) repeat-x;
            mask: var(--chev) 0 50% / var(--chev-w) var(--chev-h) repeat-x;
}

/* A length, so the script reads it in px (fitRoad). */
@property --chev-p {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

/* A road pointing back, towards the car behind (pointRoads). */
.obs-gapline.is-turned {
    transform: scaleX(-1);
}

/* The chevrons, unlit: the road's grey, which the cars share. */
.obs-gapline::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--road);
}

/*
    The light fills the road: the chevrons light one after another from the
    road's start and stay lit until it is full, hold, and fade out together
    -- then again, every --chev-cycle (1.65s) whatever the count, so the two
    roads, and every plate, fill together on the script's one clock. (It
    was one chevron lit at a time, blinking in and out within each step,
    which was distracting.)

    One road's width of --light, slid onto the road a tile at a time
    (obs-chev-fill, steps()) from just off the road's start, so a fill
    starts on one dark step, 150ms, after the last one has faded (it was
    two, ~0.3s, which read as a pause; then none, lit the moment the fade
    ended, which was too soon), and its
    last step lands it on the road, all lit, where it holds while it fades
    (obs-chev-fillfade, 350ms: it was 500). A cycle ends dark, which is
    where a road changes direction or colour (turnRoad waits for it), so a
    fill is never cut short and a new one always starts from the beginning.

    Pure --light, nothing mixed in: a white-mixed middle flashed pale pink
    on the red. Only on a road with a light (.is-up / .is-down); unlit, the
    chevrons are just grey.
*/
.obs-gapline.is-up::after,
.obs-gapline.is-down::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    background: var(--light);
    animation:
        obs-chev-fill var(--chev-cycle) steps(var(--chev-count), end) infinite,
        obs-chev-fillfade var(--chev-cycle) ease-in infinite;
}

/*
    Stepped (the shorthand's steps()) a chevron at a time from just off the
    road's start to the road full over the first 900ms -- 150ms a step with
    six chevrons, the usual count -- and held there to the end; held for
    400ms and faded over the last 350ms. The stops are those times over
    the 1.65s cycle.
*/
@keyframes obs-chev-fill {
    0%      { transform: translateX(-100%); }
    54.545% { transform: translateX(0); }
    100%    { transform: translateX(0); }
}

@keyframes obs-chev-fillfade {
    0%      { opacity: 1; }
    78.788% { opacity: 1; }
    100%    { opacity: 0; }
}

/*
    No filling under reduced motion: a lit road's chevrons lit, faintly,
    all at once. The same selectors as the light's, so this outranks it.
*/
@media (prefers-reduced-motion: reduce) {
    .obs-gapline.is-up::after,
    .obs-gapline.is-down::after {
        animation: none;
        width: 100%;
        opacity: .6;
    }
}

/* The figure, with room either side of it on the road. */
.obs-gapfig {
    position: relative;
    flex: none;
    margin: 0 calc(.8rem * var(--note));
    font-size: calc(2.5rem * var(--note));
}

/*
    "+0.21" in red when the gap grew, "-0.34" (a real minus) in green when
    it closed: the fastest lap's gain (.obs-notegain), told the same way,
    in its style (the rule above). Under the gap, centred on it whatever
    its width, in room the plate keeps for it (.obs-note-gap
    .obs-notetext's bottom padding) whether or not there is a change to
    show. Empty until the first change, and emptied when the two swap
    places (showGap).

    Its history: under the gap at first; then above it, between the names,
    where it took no height; then lowered towards the gap, where the right
    spacing was 1.48 --note units from its digits to the gap's (closer read
    as crowded). Under again, so it can never meet a name, at that same
    spacing: from the gap's baseline to the top of its digits (--below,
    measured). Placed by its box's top, not a translate, since the flick
    (showCell) animates transform.
*/
.obs-gapdelta {
    --gain-scale: 1.33;
    --below: calc(.4rem * var(--note));
    position: absolute;
    top: 100%;
    left: -2em;
    right: -2em;
    margin-top: var(--below);
    line-height: 1;
    text-align: center;
    will-change: transform, opacity;   /* it flicks, as the tower's cells do */
}

.obs-gapdelta.is-up   { color: var(--red-lit); }
.obs-gapdelta.is-down { color: var(--green-lit); }

/*
    The number is the point of the whole notification, so it is the biggest
    thing on the plate -- bigger than the names either side of it and bigger
    than the headline the other notifications use.
*/
.obs-gapvalue {
    display: block;
    font-size: 1em;     /* the figure's (.obs-gapfig) */

    /*
        Thickened with a stroke rather than a heavier weight, because the weight
        does nothing here: the stack names "Segoe UI Semibold" -- one face, not a
        family -- so 700, 800 and 900 all render identically (measured: same ink,
        same width). The stroke is on the same letterforms the rest of the
        overlay uses, needs no other face installed on the streaming machine, and
        in em it scales with the canvas like everything else.

        The weight below is kept so the intent still reads, and so it would take
        effect if the stack ever gains a heavier face.
    */
    font-weight: 900;
    -webkit-text-stroke: .04em currentColor;
    paint-order: stroke fill;

    letter-spacing: .02em;
    color: var(--yellow);
    white-space: nowrap;
    will-change: transform, opacity;   /* it flicks into and out of laps, as the tower's cells do */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
    text-align: center;     /* in a width set for its shape, not its digits (setGapText) */
}

/* setGapText's hidden copy, measured at 100px. */
.obs-gapvalue.obs-gapsizer {
    position: absolute;
    top: 0;
    left: 0;
    width: auto;
    font-size: 100px;
    visibility: hidden;
    pointer-events: none;
}

/*
    The new leader is in the tower's leader red, because it is the tower's P1
    block announced -- the same reasoning as the fastest lap being purple. That
    is also the red flag's red; the artwork is what tells them apart, a P1
    block against a flag.
*/
.obs-note-start   { --note-strip: var(--green);  --note-label: var(--green-lit);  --note-art: var(--green); }
.obs-note-pause   { --note-strip: var(--red);    --note-label: var(--red-lit);    --note-art: var(--red); }
.obs-note-over    { --note-strip: var(--text-bright); --note-label: var(--text-soft); }
/*
    The fastest lap's label and stopwatch in --purple-lit, the tower's
    fastest-time purple, not --purple: the full purple is 3.1:1 on the plate,
    the darkest colour on the board, and on a stream (video keeps colour at
    half resolution; edges ride on brightness) thin purple text dissolved.
    --purple-lit is 5.3:1. The strip keeps --purple, being a solid block.
*/
.obs-note-fastest { --note-strip: var(--purple); --note-label: var(--purple-lit); --note-art: var(--purple-lit); }
.obs-note-gap     { --note-strip: var(--yellow); --note-label: var(--yellow-lit); }
.obs-note-leader  { --note-strip: var(--red);    --note-label: var(--red-lit);    --note-art: var(--red); }
.obs-note-improved { --note-strip: var(--blue); --note-label: var(--blue-lit); --note-art: var(--blue); }
.obs-note-standings { --note-strip: var(--silver); --note-label: var(--text-mid); --note-art: var(--silver); }

/*
    A car going OUT, and coming back (reviewOut): the car's place in the
    tower's block (placeArt), as the new leader has the P1 block. OUT is
    the tower's greyed row said again -- grey strip, grey block, a dimmed
    name. REJOINED is the same plate lit: the green flag's green, for racing
    again, and the block white. The one difference is the point.
*/
.obs-note-out    { --note-strip: var(--text-dark); --note-label: var(--text-out);  --note-art: var(--idle); }
.obs-note-out .obs-notemain     { color: #c9c9d1; }
.obs-note-out .obs-noteplace    { fill: #7a7a84; }

.obs-note-rejoin { --note-strip: var(--green);     --note-label: var(--green-lit); --note-art: var(--text-bright); }
.obs-note-rejoin .obs-noteplace { fill: #0c0c0e; }

/*
    The number in the block is real bold (the rule with .obs-clock), placed
    by placeArt on its ink -- measured in the canvas, which only has the
    face's default digits, so these are left at the default too.
*/

/*
    The race result, as a podium: its own kind of plate, not a restyled
    "over" note. Gold strip and label rather than the chequered flag's white:
    the flag beside them says the race is over, and the plate is about who
    won it.

    The strip is gold leaf to match the winner's medal: the same metal
    (--gold-leaf, run down it rather than across, since it is tall and thin),
    a shine that runs top to bottom, and sparkles of its own (added by the
    script). Its own stacking context, so they sit on the metal.
*/
.obs-note-podium .obs-notestrip {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    background: linear-gradient(170deg, var(--gold-leaf));
}

.obs-note-podium .obs-notestrip::before {
    content: "";
    position: absolute;
    inset: 0 -50%;
    z-index: -1;
    background: linear-gradient(180deg, transparent 38%, rgba(255, 255, 255, .9) 50%, transparent 62%);
    transform: translateY(-110%);
    animation: obs-shine-down 3.6s cubic-bezier(.4, 0, .2, 1) .3s infinite;
}

@keyframes obs-shine-down {
    0%   { transform: translateY(-110%); }
    40%  { transform: translateY(110%); }
    100% { transform: translateY(110%); }
}

/* Sized to the strip's width, not the medal-sized 26%. */
.obs-notestrip .obs-sparkle {
    width: 80%;
    margin-left: -40%;
}

.obs-note-podium { --note-label: var(--gold-lit); }

/*
    The flag grows a little with the plate, which is twice the height of the
    others, but only within the column every plate's picture shares
    (.obs-noteart): .75rem either side of it, where the others have 1.15.
*/
.obs-note-podium .obs-noteart svg {
    width: calc(4.4rem * var(--note));
    height: calc(4.4rem * var(--note));
}

/* The winner: the biggest name the overlay ever puts on screen. */
/*
    The winner's box is exactly as tall as the name and the result beside it
    together, and square. Set by arithmetic rather than stretched: a flex item
    stretched to its row's height does not take a width from aspect-ratio,
    so it came out tall and thin. The two lines' heights are pinned below
    (font size times line height, plus the gap between), and the box is
    their sum -- change one of those numbers and change --podwin-h with it.
*/
.obs-podwin {
    --podwin-h: calc((2.7rem * 1.05 + .2rem + 1.2rem * 1.3) * var(--note));
    display: flex;
    align-items: center;
    gap: calc(.9rem * var(--note));
    margin-top: calc(.35rem * var(--note));
}

.obs-podwho {
    display: flex;
    flex-direction: column;
    min-width: 0;          /* stops at the plate (see "name boxes") */
}

.obs-podwin .obs-podname {
    font-size: calc(2.7rem * var(--note));
    font-weight: 700;
    line-height: 1.05;
    -webkit-text-stroke: .02em currentColor;
    paint-order: stroke fill;
}

.obs-podwin .obs-podresult {
    margin-top: calc(.2rem * var(--note));
    font-size: calc(1.2rem * var(--note));
    line-height: 1.3;
    color: var(--text-mid);
}

.obs-podwin .obs-podpos {
    flex: none;
    width: var(--podwin-h);
    height: var(--podwin-h);
    font-size: calc(3rem * var(--note));
    border-radius: calc(.3rem * var(--note));
}

/*
    2nd and 3rd: a rule under the winner, then a line each. The row gap is
    only ever between those two lines; .7, up from .4, gives 3rd a little
    more room under the bigger 2nd.
*/
.obs-podrest {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;     /* the name's column stops at the plate (see "name boxes") */
    align-items: center;
    column-gap: calc(.7rem * var(--note));
    row-gap: calc(.7rem * var(--note));
    margin-top: calc(.7rem * var(--note));
    padding-top: calc(.7rem * var(--note));
    border-top: .1rem solid rgba(255, 255, 255, .12);
}

/* Each row's three cells sit straight in the grid, so the columns line up. */
.obs-podrow {
    display: contents;
}

.obs-podrest .obs-podname {
    font-size: calc(1.45rem * var(--note));
    font-weight: 700;
}

.obs-podrest .obs-podresult {
    padding-left: calc(1.5rem * var(--note));
    font-size: calc(1.2rem * var(--note));
    color: var(--text-out);
}

/*
    2nd between the winner and 3rd: its name 1.85, between theirs (2.7 and
    1.45) and nearer 3rd's -- it was halfway, 2.05, and asked to be a
    little smaller -- and its silver box grown with it, in the same
    proportion to the name as 3rd's bronze. The boxes keep one left edge
    in their column, as the winner's does.
*/
.obs-podrest .obs-podpos-2 {
    font-size: calc(1.65rem * var(--note));
}

.obs-podrest .obs-podpos-2 + .obs-podname {
    font-size: calc(1.85rem * var(--note));
}

.obs-podname,
.obs-impname {
    letter-spacing: .02em;
    text-transform: uppercase;
    white-space: nowrap;
}

/*
    Improvements: everyone who beat their best qualifying run so far, a line
    each -- car number, name, the result, and by how much in green -- in the podium's
    2nd and 3rd style and sizes, closer together, so ten fit (every car in a heat
    can improve). Names shrink to fit their column (.obs-fit), as ever.
*/
.obs-note-improved .obs-notetext,
.obs-note-standings .obs-notetext {
    flex: 1 1 auto;        /* the plate's width, not the lines': the names' column takes the rest */
    min-width: 0;
}

/*
    A grey rule between drivers, in the tower's divider (.1rem, white at
    14%), so each line reads across to its own figures. Unbroken across the
    plate: the columns are spaced by each cell's padding, not a gap, so the
    rule runs through them.
*/
.obs-imprest {
    --imp-pad-y: calc(.22rem * var(--note));
    --imp-pad-x: calc(1rem * var(--note));
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;     /* the name's column stops at the plate (see "name boxes") */
    align-items: stretch;
    margin-top: calc(.45rem * var(--note));
}

.obs-improw {
    display: contents;
}

.obs-impcar,
.obs-impname,
.obs-impresult,
.obs-impgain,
.obs-stclass,
.obs-stplace {
    display: flex;
    align-items: center;
    padding: var(--imp-pad-y) var(--imp-pad-x);
}

.obs-improw + .obs-improw > * {
    border-top: .1rem solid rgba(255, 255, 255, .14);
}

/*
    The car number first, as the tower has it: small, grey, tabular, centred
    in a column as wide as the widest, and close to its name.
*/
.obs-impcar {
    justify-content: center;
    min-width: 1.6em;
    padding-left: 0;
    padding-right: calc(.6rem * var(--note));
    font-size: calc(1.2rem * var(--note));
    font-weight: 600;
    color: var(--text-label);
    font-variant-numeric: tabular-nums;
}

/*
    The name and the result in the podium's lettering (.obs-podname,
    .obs-podresult, which these share) at its 2nd and 3rd's sizes; the
    gain in every gain's (.obs-notegain, .obs-gapdelta).
*/
.obs-impname {
    padding-left: 0;
    font-size: calc(1.45rem * var(--note));
    font-weight: 700;
    line-height: 1.2;
}

.obs-impresult {
    font-size: calc(1.2rem * var(--note));
    color: var(--text-out);
}

.obs-impgain {
    justify-content: flex-end;
    padding-right: 0;
    color: var(--green-lit);
}

.obs-impgain {
    font-weight: 700;
    color: var(--green-lit);
}

/*
    Standings: everyone in the race and their class place now, in
    Improvements' lines (.obs-imprest: the same grid, sizes, rule, car
    number and name), with the class where the result is there, small and
    grey as it is. The place last, right-aligned, in the bold face the
    tower's places use, then how far it moved: a triangle and the count,
    green up and red down (the -lit tints, as text), nothing for holding, in
    a column of its own width so the places line up.
*/
.obs-stclass {
    font-size: calc(1.2rem * var(--note));
    font-weight: 600;
    color: var(--text-out);
    white-space: nowrap;
}

.obs-stplace {
    justify-content: flex-end;
    align-items: baseline;
    gap: calc(.3rem * var(--note));
    padding-right: 0;
    white-space: nowrap;
}

.obs-stnow {
    font-family: var(--bold-face);
    font-size: calc(1.45rem * var(--note));
    font-weight: 700;
}

/*
    The triangle is the digits' height exactly (--digit-em, measured by the
    script: sizeMoveArt), its bottom on their baseline -- an svg's baseline
    is its bottom -- so it runs from the foot of the number to its top.
*/
.obs-stmove {
    display: inline-flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: .14em;
    min-width: 2.2em;
    font-size: calc(1.15rem * var(--note));
    font-weight: 700;
}

.obs-stmove svg {
    display: block;
    height: calc(var(--digit-em, .71) * 1em);
    width: calc(var(--digit-em, .71) * 1em * 10 / 8.66);
}

.obs-stmove.is-up { color: var(--green-lit); }
.obs-stmove.is-down { color: var(--red-lit); }

.obs-podresult,
.obs-impresult {
    font-weight: 600;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/*
    The place, in a medal box -- the .obs-gappos construction (see there), in
    the medal's colour, sized by its own font so the winner's is bigger.

    Its own stacking context (isolation), so the shine and glitter can sit at
    z-index -1: over the box's metal, under its digit. The shine is a bright
    band swept diagonally across (::before) every few seconds; the glitter is
    a handful of four-point sparkles (.obs-sparkle, added by the script)
    twinkling on their own clocks, so they never fire together. Each medal's
    cycle is offset, so the three do not flash in step.
*/
.obs-podpos {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    font-size: calc(1.3rem * var(--note));
}

/*
    The medals, as metal rather than flat paint: a diagonal gradient through
    a dark edge, the colour, a bright highlight and back, which is what reads
    as a polished surface at this size. The flat --gold / --silver / --bronze
    are the middle of each. Gold's stops are --gold-leaf, shared with the
    podium's strip.
*/
.obs-podpos-1 {
    background: linear-gradient(135deg, var(--gold-leaf));
}
.obs-podpos-2 {
    background: linear-gradient(135deg, #7c828c 0%, var(--silver) 28%, #ffffff 48%, var(--silver) 64%, #858a94 100%);
}
.obs-podpos-3 {
    background: linear-gradient(135deg, #7a4a22 0%, var(--bronze) 28%, #ffd2a8 48%, var(--bronze) 64%, #84512a 100%);
}

.obs-podpos::before {
    content: "";
    position: absolute;
    inset: -20%;
    z-index: -1;
    background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, .85) 50%, transparent 65%);
    transform: translateX(-120%);
    animation: obs-shine 3.2s cubic-bezier(.4, 0, .2, 1) infinite;
}

.obs-podpos-2::before { animation-delay: .6s; }
.obs-podpos-3::before { animation-delay: 1.1s; }

@keyframes obs-shine {
    0%   { transform: translateX(-120%); }
    35%  { transform: translateX(120%); }
    100% { transform: translateX(120%); }
}

.obs-sparkle {
    position: absolute;
    z-index: -1;
    width: 26%;
    aspect-ratio: 1 / 1;
    background: #fff;
    clip-path: polygon(50% 0, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0 50%, 40% 40%);
    opacity: 0;
    transform: scale(.2);
    animation: obs-twinkle 2.4s ease-in-out infinite;
}

@keyframes obs-twinkle {
    0%, 55%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); }
    70%           { opacity: 1; transform: scale(1) rotate(45deg); }
    85%           { opacity: 0; transform: scale(.3) rotate(90deg); }
}

@media (prefers-reduced-motion: reduce) {
    .obs-podpos::before,
    .obs-note-podium .obs-notestrip::before,
    .obs-sparkle {
        animation: none;
    }
}

/* -- the timing tower ----------------------------------------------------- */

.obs-tower {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;

    /*
        --tower is the tower's size, set by the layout. Every measurement below
        is multiplied by it, so resizing the tower is that one number -- and it
        is a variable rather than a transform because the rows carry their own
        transforms when they slide, and a scaled parent would throw those off.
    */
    gap: calc(.3rem * var(--tower));
}

/*
    Nothing is running -- the state bar is black, and any winner has been
    announced -- so there is no order worth putting on a broadcast and the
    tower gets off the feed. The header and the corner panel stay: between
    races they are what says which race is next and how long there is until it.

    The rows stack on and off one at a time (see setTowerShown); this class is
    only the resting state at the end of that, and what hides a row created
    while the tower is off.

    Transparent rather than taken out of the layout, for two reasons. The rows
    keep their measured positions, so renderTower's slide still works while the
    tower is out of sight and it comes back in the right order rather than
    rearranging itself in view. And you cannot animate out of display: none.
    This is a browser source over a video feed, so transparent is gone -- no
    visibility or pointer-events guard needed.
*/
.obs-tower.is-out .obs-row {
    opacity: 0;
}

.obs-row {
    display: flex;
    align-items: stretch;

    /*
        Not clipped, because .obs-fl below hangs outside the right edge on
        purpose and overflow: hidden would cut it off. What the clip used to do
        was round the position block at the left end, and .obs-pos now carries
        that radius itself.
    */
    position: relative;

    /*
        Promoted for the whole of its life, not just while it slides. Creating
        the layer at the start of a slide and throwing it away at the end makes
        the browser re-rasterise the row both times, and the second one lands
        exactly as the row settles -- seen as a snap on the edges, worst on the
        leader's red block, whose stroke is clipped by the radius above.
        Keeping the layer means the row is rasterised once and only moved.

        The cost is a pixel: a promoted layer snaps to whole device pixels,
        while the corner panel sits on the fractional layout position, so the
        tower's bottom edge lands one pixel above it. Promoting the panel too
        does not close the gap. A pixel nobody can see is the better trade
        against a snap everybody can.
    */
    will-change: transform;
}

/*
    The row's dark plate, drawn as a layer rather than as the row's own
    background. That is what lets the FL badge tuck in behind it: a child can
    never paint under its parent's background, but it can paint under a
    sibling layer. Both sit at negative z-index inside the row -- badge at -2,
    plate at -1 -- so the plate covers the badge's left edge and the row's own
    text still draws over both.

    The row makes a stacking context of its own (will-change, above), so those
    negatives are scoped to the row and cannot fall behind the tower.
*/
.obs-row::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: calc(.35rem * var(--tower));
    box-shadow: 0 .2rem .8rem rgba(0, 0, 0, .45);

    /*
        Opaque, and it must stay opaque through a slide. A row used to be made
        solid only while moving and dropped back to .98 on landing, which
        switched its text between subpixel and greyscale antialiasing at the
        exact moment it settled -- visible as a snap over the text. One
        background throughout removes the switch, and removes the reason the
        override existed: two opaque plates crossing cannot double-darken.
    */
    background: rgb(12, 12, 14);
}

/*
    The two badges that hang off a row's right-hand end: FL, on whoever holds
    the fastest lap, marked the way F1 marks it -- in the same purple as the
    fastest-lap notification, because they are the same fact said twice --
    and the chequered flag a finished car wears in its place.

    They hang outside the row rather than sitting in it, and that is forced
    rather than stylistic: the tower's rows stretch to one width and
    .obs-name takes up the slack, so a badge that took part in the layout
    would widen every row by its width instead of making one row jut out.
    Absolute, with the row as its containing block, keeps it out of that sum
    entirely.

    They are one size, and every size here is shared, so they cannot drift
    apart: --badge-cols checks across and --badge-rows down (on .obs-tower,
    below), each --badge-cell of the tower's rem, inside a rim of --badge-rim
    on the three sides clear of the row. The flag fills that with checks and
    a white rim; FL with purple, rim and all -- the rim is there to make it
    exactly the flag's size.

    Each check is a whole number of screen pixels (--badge-cell-px, set by
    snapFlags), which is what makes them all the same size. Snapping rounds
    each edge, so a check of, say, 10.52 screen pixels came out 11 in some
    rows and columns and 10 in others: measured, the top black check was
    11 x 11 and the bottom ones 10 x 10, and the rim 2 pixels on top and 3
    below. With a whole number every edge rounds the same way, wherever on
    the screen the badge lands. The rim (--badge-rim-px) is snapped too.
    Before the script has run -- or without it -- the unsnapped sizes stand
    in.

    The outer corners are rounded by exactly the rim's width, so each arc is
    centred on the corner of the check inside it and the rim stays the same
    width all the way round the corner. A bigger radius -- FL's old one,
    about twice the rim -- cut in towards the check's square corner and left
    the rim a pixel or so wide on the diagonal, which read as the black check
    breaking into the border.

    Drawn over the row's plate (z-index 0). They start below where the
    plate's corners curve, so nothing needs to reach under them, and under
    the plate they sat in its shadow, which darkened the flag's left column
    and made those checks look squashed.

    They come and go by sliding out from behind the row and back (the
    script's slideBadge), never by fading. Both sit in one slot the badge's
    size, at the row's end; the slot is clipped at the row's edge, so a
    badge slid its own width to the left -- where each rests, until the
    script puts it out -- is behind the row and unseen. The clip reaches
    out on the other three sides far enough for the badge's shadow.
*/
.obs-badge-slot {
    --ring: var(--badge-rim-px, calc(var(--badge-rim) * 1rem * var(--tower)));

    position: absolute;
    z-index: 0;

    /*
        Flush: its left edge lands on the right edge every other row ends at,
        so the tower keeps one straight edge and the badge is the only thing
        past it -- and with no rim on that side, it meets the row.

        A pixel inside it, in fact, over the row's end (the badge is drawn
        over the plate). Exactly at the edge, the row's plate and the badge
        (and the slot's clip) all ended on the same fractional pixel, each
        half-covering it, and the background showed through between them:
        a hairline, white in a browser, between the FL badge and the tower.
        Overlapped, the plate's edge is under the badge and the clip's is
        over the plate, so there is nothing behind either to show.
    */
    left: calc(100% - 1px);

    /* Centred on the row by its own size, the rim included. */
    top: 50%;
    margin-top: calc(var(--badge-rows) * var(--cell) / -2 - var(--ring));
    width: calc(var(--badge-cols) * var(--cell) + var(--ring));
    height: calc(var(--badge-rows) * var(--cell) + 2 * var(--ring));
    clip-path: inset(-1.5rem -1.5rem -1.5rem 0);
    pointer-events: none;
}

.obs-fl,
.obs-chq {
    position: absolute;
    left: 0;
    top: 0;
    box-sizing: content-box;
    width: calc(var(--badge-cols) * var(--cell));
    height: calc(var(--badge-rows) * var(--cell));
    padding: var(--ring) var(--ring) var(--ring) 0;
    border-radius: 0 var(--ring) var(--ring) 0;
    box-shadow: 0 .2rem .8rem rgba(0, 0, 0, .45);

    /*
        Away behind the row until the script slides it out -- its own width
        and a margin more. At exactly its own width its right edge sat on the
        slot's clip, and while the window was being resized the rounding let
        a hairline of it through: a thin purple strip at the end of every
        row, from each one's hidden FL. The margin keeps it well clear, and
        never less than 4px, since the rem shrinks with the window. The
        script slides from and to the same place (--badge-away), so this is
        the only copy.
    */
    --badge-away: translateX(calc(-100% - max(.5rem, 4px)));
    transform: var(--badge-away);
}

/*
    FL: the flag's box, filled purple, with the letters FL in white. Its
    size is the shared rule's. (It carried the notification's stopwatch for
    a while; the letters scale better.)
*/
.obs-fl {
    background: var(--purple);
    color: #fff;
}

/*
    The letters, centred on the whole purple box -- rim included, which has
    none on the left, so the content box's centre is off by half a rim --
    and centred on their ink, not their line box.

    Real bold (--bold-face), with no letter-spacing, which would leave space
    after the L. line-height 1 sets the line box to the em; the padding then
    move the ink's middle onto the box's: F and L are both flat capitals, so
    their ink is exactly the cap height, from the baseline up, and their
    side bearings differ. Measured in Segoe UI Bold (canvas, 200px); change
    the face and measure again.
*/
.obs-fltext {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(1.1rem * var(--tower));
    line-height: 1;

    /*
        Padding one side moves the centre half as far the other way. The
        ink sits .065em low in the line box (baseline .915em down, caps .7em
        tall) and .032em right of the advance's middle (F's left bearing is
        wider than L's right), so these are twice that.
    */
    padding-bottom: .13em;
    padding-right: .064em;

    /*
        And the last fraction of a pixel, measured by the script at this
        size (centreFlText) -- the browser rounds the font's metrics to
        whole pixels, differently at every size.
    */
    translate: var(--fl-dx, 0px) var(--fl-dy, 0px);
}

/*
    Where the script lays things out of sight to measure them: a tower row
    for the columns' widths (setColumnWidths), a frame's label
    (centreFrameLabels), plates for the room they need (needsTall). Beside
    the boxes, never in one, which would stop it being :empty and open its
    frame.
*/
.obs-probe {
    position: absolute;
    left: 0;
    top: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    visibility: hidden;
    pointer-events: none;
}

.obs-probe .obs-note {
    transform: none;
}

/* The label's copy is measured without its letter-spacing (centreFrameLabels). */
.obs-probe.obs-labelprobe {
    letter-spacing: 0;
}

/* The script's measuring copy, measured without the nudge. */
.obs-flprobe {
    translate: none;
}

/*
    Finished: the chequered flag, where the FL badge was. The two cannot
    share the spot, and a finished car's fastest lap is already said by its
    purple time (.shows-best), so FL slides back in and the flag slides out.

    The checks are elements, a grid of spans the script builds (buildFlag),
    each dark one filled -- not a drawing. A background image of the checks
    was scaled to the flag, and at a display scale like 125% every edge
    between two checks landed part-way through a pixel and came out as a
    grey seam. A box's background is snapped to whole pixels instead. The
    cells are only inside the rim, each filling itself, so the rim sits
    outside the checks and never over them.
*/
.obs-chq {
    display: grid;
    grid-template-columns: repeat(var(--badge-cols), var(--cell));
    grid-template-rows: repeat(var(--badge-rows), var(--cell));

    /*
        The rim: white, the same as the checks. (The position box's grey and
        a dimmer white were both tried, and full white asked for back.)
    */
    background: #fff;
}

/*
    Down the tower the flags read as one chequerboard. With an even number
    of rows of checks, as now, that is free: each flag ends on the other
    colour to the one it starts on, so every flag starting white carries
    the pattern on. With an odd number each ends on its starting colour, so
    every other place would have to start on black.

    --badge-rows is that number, and --badge-cols the checks across; the
    grid and the script (buildFlag, alternateFlags) both take the flag's
    shape from these two, and FL its size (above). --badge-cell and
    --badge-rim are the sizes of a check and the rim, as numbers of the
    tower's rem, which snapFlags rounds to whole screen pixels. Four by
    four, square: it was three across, and four asked for. Odd rows, and
    every other
    place wears the black-first layer (.obs-chq-alt, .is-alt -- set by the
    row's place, in updateRow), and a car changing places fades from one to
    the other rather than popping (fadeFlags) -- a Web Animation, for the
    same reason as the leader's red: renderTower moves the rows to reorder
    them, and a move cancels a CSS transition before it starts. Even, and
    no row is ever .is-alt, so nothing changes on a sort.
*/
.obs-tower {
    --badge-cols: 4;
    --badge-rows: 4;
    --badge-cell: .5;
    --badge-rim: .12;

    /* One check, snapped: the flags' grid and the improving strip's width. */
    --cell: var(--badge-cell-px, calc(var(--badge-cell) * 1rem * var(--tower)));
}

.obs-chq-cell {
    background: #fff;
}

.obs-chq-cell.is-dark {
    background: #111;
}

/*
    Inside the rim, over the checks only: the same grid, its colours the
    other way round.
*/
.obs-chq-alt {
    position: absolute;
    inset: var(--ring) var(--ring) var(--ring) 0;
    display: grid;
    grid-template: inherit;
    opacity: 0;
    background: #111;
}

.obs-chq-alt .obs-chq-cell {
    background: #111;
}

.obs-chq-alt .obs-chq-cell.is-dark {
    background: #fff;
}

.obs-row.is-alt .obs-chq-alt {
    opacity: 1;
}

/* The leader gets the accent, the way a timing tower marks P1. */
.obs-row.is-leader .obs-pos {
    background: var(--red);
    color: #fff;
}

.obs-pos {
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(3.2rem * var(--tower));
    padding: calc(.55rem * var(--tower)) 0;
    background: rgba(255, 255, 255, .1);

    /* The row no longer clips, so the block rounds its own outer corners. */
    border-radius: calc(.35rem * var(--tower)) 0 0 calc(.35rem * var(--tower));
    font-size: calc(1.7rem * var(--tower));
    font-variant-numeric: tabular-nums;

    /*
        No transition here: the leader's red is faded by the script
        (fadeLeaderBadges), because the rows are moved in the DOM to reorder
        them and a move cancels a CSS transition before it starts.
    */
}

.obs-car {
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(2.6rem * var(--tower));
    font-size: calc(1.05rem * var(--tower));
    font-weight: 600;
    color: var(--text-label);
    font-variant-numeric: tabular-nums;
    --car-divider: .1rem;
    border-right: var(--car-divider) solid rgba(255, 255, 255, .08);

    /* For the improving strip's slot, below. */
    position: relative;
}

/*
    Qualifying, round 2 on: the green strip down the start of the name's
    cell, on a car on course to beat its best run so far at the meeting
    (the script's markImprove). One check of the chequered flag wide (the
    snapped --cell the flags use), the row's height, against the
    right of the divider before the name -- this cell's border-right, so
    the slot starts that border's width (--car-divider) past the cell. Kept in this cell
    rather than the name's, whose text is rewritten and measured (updateRow,
    fitTowerNames); it lies over the name's left padding, clear of the name.

    It slides out from the divider towards the name and back behind it
    (slideImprove): the slot is clipped at its own edges, so the strip
    resting past its left edge (--improve-away) is behind the divider and
    unseen. A margin more than its own width, as the badges' --badge-away
    has, so a resize's rounding cannot let a hairline of it through. Solid
    colour, so the full --blue, not the -lit tint. (It was green, and
    clashed with the flag's and the progress bar's.)
*/
.obs-improve-slot {
    position: absolute;
    top: 0;
    left: calc(100% + var(--car-divider));
    bottom: 0;
    width: var(--cell);
    overflow: hidden;
    pointer-events: none;
}

.obs-improve {
    position: absolute;
    inset: 0;
    background: var(--blue);
    --improve-away: translateX(calc(-100% - max(.25rem, 2px)));
    transform: var(--improve-away);
}

.obs-name {
    display: flex;
    align-items: center;
    flex: 1 1 auto;
    padding: 0 calc(.7rem * var(--tower)) 0 calc(1.2rem * var(--tower));
    font-size: calc(1.7rem * var(--tower));
    font-weight: 700;
    letter-spacing: .01em;
    text-transform: uppercase;
    white-space: nowrap;
}

/*
    The face of the result, the projection and the stat column: the part
    of the cell that flicks, filling it, so the cell -- whose left edge is
    the divider before it -- stays put, as .obs-pos does round .obs-posnum.
    The same height as the cell (none has padding top or bottom), so the
    flick's 35% is the same distance it was; laid out as the cell lays out
    its figure, so the cell's alignment still places it.
*/
.obs-face {
    display: flex;
    flex: 1 1 auto;
    align-self: stretch;
    align-items: center;
    justify-content: inherit;
}

/*
    The parts that flick when their figure changes (showCell): the faces of
    the result, the projection and the stat column, and the place number. (Not the corner clock's digits,
    .obs-digit: they flick once a minute, which is not worth thirty layers.) Promoted for the whole of their life,
    for the same reason as the row they sit in: a transform or opacity
    animation gives an element a layer of its own while it runs, and making
    and dropping that layer around every 180ms flick re-rasterised the cell's
    text at both ends -- a shimmer on every change, which in a busy race is
    the tower stuttering. With a layer throughout, the text is drawn once and
    only moved.
*/
.obs-face,
.obs-posnum {
    will-change: transform, opacity;
}

.obs-result {
    display: flex;
    align-items: center;
    justify-content: flex-end;

    /*
        Just wide enough for "25 / 301.68" -- two-digit laps and a three-digit
        time, which is most of a five-minute race -- so the column does not
        widen, and the tower with it, as the numbers grow. Measured; trimming it
        brings that back. Early on, with "3 / 36.00", the slack sits to the
        left of the numbers, which is why the gap after the name is bigger then.
        Wider than that, every row takes --result-w, sized in digits for the
        race (sizeColumnsForRace, fitFigures), so the column lines up down
        the tower.
    */
    min-width: max(calc(7.6rem * var(--tower)), var(--result-w, 0px));
    padding: 0 calc(1.2rem * var(--tower)) 0 calc(.7rem * var(--tower));
    font-size: calc(1.5rem * var(--tower));
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/*
    The stat column: the gap to the car ahead, the gap to the leader, or the
    driver's own fastest lap, taking turns every STAT_MS (see cycleStat).

    As tight as it can be, because every rem here widens the whole tower. A
    fixed width sized for the widest thing it shows -- a gap to two places,
    "+12.34" (measured: 4.19rem of tabular digits; 4.24 for headroom) -- so
    the tower does not change width with the mode or the numbers. A gap over
    99.99s is a lap or more by then, and reads in laps. Which of the three it
    is showing is said by the leader's cell (INTERVAL / LEADER; the leader has
    no gap of its own) and by the
    format (gaps are signed, laps are not), not by a header the tower would
    need extra height for.
*/
.obs-stat {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    box-sizing: content-box;

    /* In rem, not em: the leader's label is a smaller font in the same box. */
    width: calc(4.24rem * var(--tower));
    --pad-l: calc(.6rem * var(--tower));
    --pad-r: calc(.8rem * var(--tower));
    --divider: .1rem;
    padding: 0 var(--pad-r) 0 var(--pad-l);
    border-left: var(--divider) solid rgba(255, 255, 255, .08);
    font-size: calc(1.3rem * var(--tower));
    font-weight: 600;
    color: var(--text-soft);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/*
    Centred in its column as it is seen, from the divider to the row's edge:
    a dash, a label or OUT in the stat column (figures sit right so their
    digits line up; these have none), and the best lap in the projection's
    place (.shows-best, below). The column's own padding, the same total,
    shared out so that with the divider on the left the two sides match --
    worked from the column's --pad-l, --pad-r and --divider, so it follows
    them. With the figures' padding a label sat 2.5px left of centre at
    fullscreen.
*/
.obs-stat.is-empty,
.obs-stat.is-label,
.obs-stat.is-out,
.obs-row.shows-best .obs-pred {
    justify-content: center;
    padding: 0 calc((var(--pad-l) + var(--pad-r) + var(--divider)) / 2)
             0 calc((var(--pad-l) + var(--pad-r) - var(--divider)) / 2);
}

.obs-stat.is-empty,
.obs-stat.is-label,
.obs-stat.is-out {
    /*
        Letter-spacing puts its space after every letter, the last included,
        so a spaced word is centred with a gap trailing it and its ink sits
        left. The same space before the first letter evens it up. It is the
        letter-spacing's own value, so it follows it; 0 for the dash.
    */
    text-indent: var(--stat-spacing, 0);
    letter-spacing: var(--stat-spacing, 0);
}

/* Words -- the mode's name, and OUT -- are spaced out a little. */
.obs-stat.is-label,
.obs-stat.is-out {
    --stat-spacing: .08em;
}

/* The mode's name, in the leader's cell: small, so it fits the same width. */
.obs-stat.is-label {
    font-size: calc(.72rem * var(--tower));
    text-transform: uppercase;
    color: var(--text-label);
}

/*
    The projected finish. Dimmer and a touch smaller than the live result, so
    the tower still reads top-to-bottom as "where everyone actually is" with
    the projection as a second thought.
*/
.obs-pred {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    /* As the result: --pred-w, sized for the race, when that is more. */
    min-width: max(calc(6.4rem * var(--tower)), var(--pred-w, 0px));
    --pad-l: calc(1.4rem * var(--tower));
    --pad-r: calc(1.2rem * var(--tower));
    --divider: .1rem;
    padding: 0 var(--pad-r) 0 var(--pad-l);
    margin-left: calc(.2rem * var(--tower));
    border-left: var(--divider) solid rgba(255, 255, 255, .08);
    font-size: calc(1.25rem * var(--tower));
    font-weight: 600;
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

.obs-pred .obs-time {
    color: var(--text-dim);
}

/*
    A row that has stopped racing (.shows-best, put on by the projection's
    flick when it writes the best lap, so all of this arrives with the lap
    time rather than under the projection it replaces): a car that has
    finished, or one that is OUT. Its best lap takes the projection's place.

    The columns keep the widths they had in the race, so a finished row and
    an OUT row line up with each other and with a running one. (The last two
    once split their combined width evenly at the finish, which left an OUT
    row's columns a different size from a running row's.)

    The best lap is as bright as the stat column's figures, now that it is
    the thing being read, and centred -- on its ink, not its advance.
    Proportional digits, as the battle note's position box found: a tabular
    1 sits in a wide cell with its space on the left, so a time starting 1
    read right of centre. It is centred as the stat column's words are
    (above); only the time is shown, since the empty laps and separator
    would pull it off centre too.
*/
.obs-row.shows-best .obs-pred {
    font-variant-numeric: proportional-nums;
    font-feature-settings: normal;
}

.obs-row.shows-best .obs-pred .obs-time {
    color: var(--text-soft);
}

.obs-row.shows-best .obs-pred .obs-laps,
.obs-row.shows-best .obs-pred .obs-sep {
    display: none;
}

/*
    A car that is OUT (readField, showFigures): the word in the stat column,
    its best lap where its projection was (.shows-best, above -- it may well
    hold the fastest lap), and the whole row greyed, the word included -- as
    F1 does, though here it lasts only until the car next crosses.

    The grey is a layer over the row (.obs-out-fill), so it can fade
    (updateRowLayers) as the other layers do: the row's plate colour, most of
    the way opaque, over everything the row draws, the leader's red included
    -- all but a fastest lap's purple time, which stays bright (below). The
    FL badge hangs outside the row and is not greyed: the lap is still
    theirs.
*/
.obs-out-fill {
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: calc(.35rem * var(--tower));
    opacity: 0;
    pointer-events: none;
    background: rgba(12, 12, 14, .62);
}

.obs-row.shows-out .obs-out-fill {
    opacity: 1;
}

/*
    The race's fastest lap, wherever the time is shown -- the stat column's
    best-lap turn in the race, the projection's place once the car has
    stopped racing: in the FL badge's purple, since it is the same fact, and
    bold. Whoever the FL badge names (shownFastestKey), so the two always
    agree. The class goes on and off in the cell's own write, at the bottom
    of its flick (showFigures, showStat), so the colour never changes in
    view. (It was a purple cell, faded on and off as a layer of the row; a
    coloured time says the same with no fading.) Bold in --bold-face, as
    the frames' tabs are.

    The projection's selector names .shows-best as well because the rule
    that makes a finished row's best lap white does, and would otherwise
    outweigh this one: seen as the fastest lap turning white, not purple,
    as its car finished.
*/
.obs-row.shows-best .obs-pred.is-fastest .obs-time,
.obs-stat.is-fastest {
    color: var(--purple-lit);    /* and real bold, with .obs-clock */
}

/*
    An OUT car can hold the fastest lap, and it stays purple at full
    strength while the rest of the row is greyed: the cell is lifted over
    the grey layer (.obs-out-fill, z-index 1), since the lap is still the
    race's fastest. Lifting the time alone would not do: the cell is a
    stacking context of its own (will-change), so nothing inside it can get
    above a sibling of the cell.
*/
.obs-row.shows-out .obs-pred.is-fastest {
    position: relative;
    z-index: 2;
}

.obs-sep {
    margin: 0 calc(.35rem * var(--tower));
    color: #6a6a74;
}

.obs-time {
    color: var(--text-mid);
}

/*
    The frame-rate meter, for ?fps=1 (liveraceboard-fps.js). A debug panel
    on the page, outside the board, so the board's fade-in does not apply to
    it; sized in px, not the board's rem, so it reads the same at any size.
    Where it sits is the layout's: each layout places .obs-fps against the
    board's own parts by CSS anchor positioning (.obs-top is --board-top,
    .obs-foot --board-foot), so no layout restates their sizes, and it
    follows them into a 16:9 frame. A browser without anchor positioning
    (before Chrome 125) puts it top left.
*/
.obs-fps {
    position: fixed;
    z-index: 2147483647;
    pointer-events: none;
    contain: content;
    min-width: 252px;
    padding: 6px 8px 7px;
    border: 1px solid #33343c;
    border-radius: 6px;
    background: rgba(10, 11, 14, .82);
    color: #e8e8ee;
    font: 12px/1.35 Consolas, "Cascadia Mono", monospace;
    font-variant-numeric: tabular-nums;
}

.obs-fps-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
}

.obs-fps-big {
    font-size: 22px;
    font-weight: 700;
    line-height: 1.1;
}

.obs-fps-dim {
    color: #9a9aa6;
}

.obs-fps-status {
    margin: 3px 0 5px;
    padding: 1px 6px;
    border-radius: 3px;
    background: #0000e0;
    color: #fff;
    font-weight: 700;
    letter-spacing: .06em;
}

.obs-fps canvas {
    display: block;
    width: 252px;
    height: 48px;
}
