html, body {
    height: 100%;
}

/* The whole app is drawn a fifth larger than a browser's default.

   Every size in this app is in `rem`, so this one number is the app's scale and nothing
   else has to know about it - change it and the type, the pattern box, the diagram and the
   splash all move together. A percentage rather than a pixel count so that a reader who
   has already set a larger default font in their browser keeps the increase.

   The media queries are the exception and cannot use it: `rem` inside a media query is
   always the browser's initial 16px, whatever this says. So every breakpoint below is
   written at 1.2x the width it would otherwise be - the content is 1.2x, so it runs out
   of room 1.2x sooner. */
:root {
    font-size: 120%;
}

/* RegExFlex theme.
   Dark is the default (this is a developer tool, and it is what most users of it want);
   light applies when the user picks it, or when the system asks for it and the user has
   not overridden that. Every token is defined here on bare `:root` so no colour exists
   only inside a media query.

   The greys are the *Flex house style and are shared with TextFlex, JwtFlex, CronFlex and
   SetFlex deliberately - they should look like the same workshop. What tells them apart is
   the accent, which is the one colour a reader sees before they have read anything:
   TextFlex is teal, JwtFlex violet, CronFlex azure, and RegExFlex is this amber. */
:root {
    /* The monospace stack, as a token rather than a repeated list.

       This is not tidiness. The pattern box and the test box are each two elements stacked one
       behind the other holding the same text - a textarea with transparent text over a layer
       carrying the colour - and the two have to be set in the *same* font or every character
       drifts away from the caret that is supposed to be in it. Naming it once is what stops the
       two halves of a pair being given different answers. */
    --rf-mono: 'JetBrains Mono', 'Cascadia Mono', 'Consolas', ui-monospace, monospace;

    color-scheme: dark;
    --rf-bg: #0d1015;
    --rf-pane: #15191f;
    --rf-pane-alt: #11151a;
    --rf-chrome: #0a0d11;
    --rf-chrome-2: #12161c;
    --rf-chrome-hover: #1e242d;
    --rf-border: #262f3a;
    --rf-border-soft: #1a212a;
    --rf-text: #dde5ee;
    --rf-muted: #8391a2;
    --rf-faint: #56626f;

    --rf-accent: #e0a33a;
    --rf-accent-bright: #f2bb5c;
    --rf-accent-ink: #241800;
    --rf-accent-dim: rgba(224, 163, 58, 0.45);
    --rf-accent-wash: rgba(224, 163, 58, 0.14);

    /* What a modal lays over the workbench. Used by an element inside the dialog as well
       as by ::backdrop, which cannot see a property declared here. */
    --rf-scrim: rgba(4, 7, 10, 0.62);

    /* The three verdict colours, used for findings and badges - so that green always means
       the same thing on this page whichever panel it turns up in.

       The warning colour has been pushed well over towards orange, and that is not a
       preference: an amber accent and an amber warning on the same page would mean the app's
       own chrome read as a caution about the pattern. Every verdict also carries a ✓, ! or ✗
       for the same reason the node hues below need one - colour alone cannot be trusted to
       carry a verdict on a page with this many hues on it. */
    --rf-good: #3ecf8e;
    --rf-good-wash: rgba(62, 207, 142, 0.13);
    --rf-warn: #f07f3c;
    --rf-warn-wash: rgba(240, 127, 60, 0.13);
    --rf-danger: #f2606a;
    --rf-danger-wash: rgba(242, 96, 106, 0.13);

    /* One hue per kind of thing a pattern can do. These are the whole idea of the workbench:
       the colour of a run of characters in the pattern box is the colour of the box that
       draws it in the diagram, of the row that explains it, and of the step that takes it in
       the trace.

       They are per *kind* and not per piece of syntax, which is the point - `\d` and `[0-9]`
       could hardly look less alike and are the same idea, so they are the same colour. There
       is no hue for a backreference: it takes the group's, because it is not a separate idea,
       it is the group quoted.

       Six hues, and they steer clear of the accent so that chrome never reads as part of a
       pattern. They cannot also steer clear of the three verdict colours - nine distinct hues
       is more than a palette has - so the rule is the other way round: a verdict never appears
       as colour alone. */
    --rf-n-literal: #6fb6f2;
    --rf-n-class: #a98bf7;
    --rf-n-any: #38c6db;
    --rf-n-group: #e37cc6;
    --rf-n-repeat: #8fd05a;
    --rf-n-anchor: #93a0b2;

    /* What a match looks like laid over the test text, and what the one being traced looks
       like. Deliberately the accent rather than a hue of its own: a match is the app's answer,
       not part of the pattern. */
    --rf-hit: rgba(224, 163, 58, 0.22);
    --rf-hit-edge: rgba(224, 163, 58, 0.55);
    --rf-cursor: #f2bb5c;

    --rf-shadow: 0 1px 0 rgba(255, 255, 255, 0.03);
}

:root[data-theme="light"] {
    color-scheme: light;
    --rf-bg: #e7ebf0;
    --rf-pane: #ffffff;
    --rf-pane-alt: #f7f9fb;
    --rf-chrome: #f9fbfc;
    --rf-chrome-2: #f1f4f7;
    --rf-chrome-hover: #e4e9ef;
    --rf-border: #ccd5df;
    --rf-border-soft: #dde4eb;
    --rf-text: #18202a;
    --rf-muted: #5a6673;
    --rf-faint: #98a3af;

    --rf-accent: #8a5a00;
    --rf-accent-bright: #a06a05;
    --rf-accent-ink: #ffffff;
    --rf-accent-dim: rgba(138, 90, 0, 0.45);
    --rf-accent-wash: rgba(138, 90, 0, 0.10);

    --rf-scrim: rgba(22, 30, 40, 0.55);

    --rf-good: #0f7a52;
    --rf-good-wash: rgba(15, 122, 82, 0.10);
    --rf-warn: #a8480a;
    --rf-warn-wash: rgba(168, 72, 10, 0.10);
    --rf-danger: #c02f33;
    --rf-danger-wash: rgba(192, 47, 51, 0.09);

    --rf-n-literal: #1668c4;
    --rf-n-class: #6247d4;
    --rf-n-any: #0d7a8c;
    --rf-n-group: #a4348c;
    --rf-n-repeat: #4a7f10;
    --rf-n-anchor: #5f6b7a;

    --rf-hit: rgba(224, 163, 58, 0.30);
    --rf-hit-edge: rgba(138, 90, 0, 0.55);
    --rf-cursor: #8a5a00;

    --rf-shadow: 0 1px 2px rgba(24, 32, 42, 0.06);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
        color-scheme: light;
        --rf-bg: #e7ebf0;
        --rf-pane: #ffffff;
        --rf-pane-alt: #f7f9fb;
        --rf-chrome: #f9fbfc;
        --rf-chrome-2: #f1f4f7;
        --rf-chrome-hover: #e4e9ef;
        --rf-border: #ccd5df;
        --rf-border-soft: #dde4eb;
        --rf-text: #18202a;
        --rf-muted: #5a6673;
        --rf-faint: #98a3af;

        --rf-accent: #8a5a00;
        --rf-accent-bright: #a06a05;
        --rf-accent-ink: #ffffff;
        --rf-accent-dim: rgba(138, 90, 0, 0.45);
        --rf-accent-wash: rgba(138, 90, 0, 0.10);

        --rf-scrim: rgba(22, 30, 40, 0.55);

        --rf-good: #0f7a52;
        --rf-good-wash: rgba(15, 122, 82, 0.10);
        --rf-warn: #a8480a;
        --rf-warn-wash: rgba(168, 72, 10, 0.10);
        --rf-danger: #c02f33;
        --rf-danger-wash: rgba(192, 47, 51, 0.09);

        --rf-n-literal: #1668c4;
        --rf-n-class: #6247d4;
        --rf-n-any: #0d7a8c;
        --rf-n-group: #a4348c;
        --rf-n-repeat: #4a7f10;
        --rf-n-anchor: #5f6b7a;

        --rf-hit: rgba(224, 163, 58, 0.30);
        --rf-hit-edge: rgba(138, 90, 0, 0.55);
        --rf-cursor: #8a5a00;

        --rf-shadow: 0 1px 2px rgba(24, 32, 42, 0.06);
    }
}

body {
    margin: 0;
    background: var(--rf-bg);
}

/* Blazor's unhandled-error bar. Hidden until the framework sets `display: block` on it by
   hand, which is why the `display: none` here is load-bearing rather than tidiness. Global,
   not scoped: every layout renders its own copy of this div, so a rule in any one .razor.css
   file would reach exactly one of them. */
#blazor-error-ui {
    position: fixed;
    bottom: 0;
    left: 0;
    z-index: 1000;
    display: none;
    box-sizing: border-box;
    width: 100%;
    padding: 0.6rem 1.25rem 0.7rem;
    background: lightyellow;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    /* The bar has a fixed light background, so its text and controls must not be re-coloured
       by a dark theme. */
    color-scheme: light only;
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 0.5rem;
    right: 0.75rem;
    cursor: pointer;
}

/* ── Shared controls ──────────────────────────────────────────────────────────
   These live here rather than in a scoped stylesheet because they are used from several
   components at once, including inside render fragments that a child component lays out -
   and scoped CSS follows the file the markup was written in, not the component it ends up
   inside. One global copy is the honest way to share them. */

/* Small ghost button: the pane and section header bars are full of these. */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    padding: 0.1875rem 0.4375rem;
    border: 1px solid transparent;
    border-radius: 4px;
    background: transparent;
    color: var(--rf-muted);
    font: inherit;
    font-size: 0.75rem;
    white-space: nowrap;
    cursor: pointer;
}

.btn:hover:not(:disabled) {
    background: var(--rf-chrome-hover);
    border-color: var(--rf-border);
    color: var(--rf-text);
}

.btn:disabled {
    cursor: default;
    opacity: 0.5;
}

/* Buttons that put something in the box rather than change how it is shown - the presets -
   so they look like buttons rather than hints. */
.btn-strong {
    border-color: var(--rf-border);
    background: var(--rf-pane);
    color: var(--rf-text);
}

.btn-strong:hover:not(:disabled) {
    border-color: var(--rf-accent-dim);
    background: var(--rf-accent-wash);
    color: var(--rf-text);
}

/* A two- or three-way switch drawn as one joined strip. Pressed rather than one-shot - the
   state is what the pane is showing. */
.seg {
    display: inline-flex;
    flex: none;
    overflow: hidden;
    border: 1px solid var(--rf-border);
    border-radius: 5px;
}

.seg > button {
    padding: 0.125rem 0.4375rem;
    border: 0;
    background: transparent;
    color: var(--rf-muted);
    font: inherit;
    font-size: 0.6875rem;
    white-space: nowrap;
    cursor: pointer;
}

.seg > button + button {
    border-left: 1px solid var(--rf-border);
}

.seg > button:hover:not(.is-on) {
    background: var(--rf-chrome-hover);
    color: var(--rf-text);
}

.seg > button.is-on {
    background: var(--rf-accent-wash);
    color: var(--rf-accent);
    font-weight: 600;
}

/* A verdict, wherever one is stated. The hue comes from a --tone the caller sets, so one
   rule covers good, warning and bad - and every badge carries a mark as well as a colour,
   because the node hues use the same part of the spectrum. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    flex: none;
    padding: 0.0625rem 0.4375rem 0.0625rem 0.375rem;
    border: 1px solid color-mix(in srgb, var(--tone, var(--rf-muted)) 40%, transparent);
    border-radius: 999px;
    background: var(--tone-wash, transparent);
    color: var(--tone, var(--rf-muted));
    font-size: 0.6875rem;
    font-weight: 600;
    white-space: nowrap;
}

.badge-good {
    --tone: var(--rf-good);
    --tone-wash: var(--rf-good-wash);
}

.badge-warn {
    --tone: var(--rf-warn);
    --tone-wash: var(--rf-warn-wash);
}

.badge-bad {
    --tone: var(--rf-danger);
    --tone-wash: var(--rf-danger-wash);
}

.badge-mark {
    font-size: 0.8125rem;
    line-height: 1;
}

/* A checkbox with its label. */
.opt {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    flex: none;
    color: var(--rf-muted);
    font-size: 0.75rem;
    cursor: pointer;
    user-select: none;
}

.opt input {
    width: 0.875rem;
    height: 0.875rem;
    margin: 0;
    accent-color: var(--rf-accent);
}

/* A name for a control that shows only a glyph. Global because the header and the library
   both need it, and scoped CSS follows the file the markup was written in. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ── Panes ───────────────────────────────────────────────────────────────────
   The panes share their chrome, and they are laid out from different components, so the
   chrome is global rather than scoped to any one of them. */
.pane {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    background: var(--rf-pane);
}

/* The pane's hue, used for the hairline under its header - enough to tie a pane to what it
   is about without colouring the text the user is actually reading. */
.pane-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: none;
    min-height: 1.875rem;
    padding: 0.1875rem 0.5rem 0.1875rem 0.625rem;
    background: var(--rf-pane-alt);
    border-bottom: 1px solid var(--rf-border-soft);
    box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--pane-hue, var(--rf-accent)) 35%, transparent);
}

.pane-title {
    margin: 0;
    flex: none;
    color: var(--rf-text);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.11em;
    text-transform: uppercase;
}

.pane-meta {
    min-width: 0;
    overflow: hidden;
    color: var(--rf-muted);
    font-size: 0.6875rem;
    font-variant-numeric: tabular-nums;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pane-spacer {
    flex: 1;
    min-width: 0.25rem;
}

.pane-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex: none;
}

/* Stacked and scrolling with the page: nothing clips its own content, and each pane's
   header follows it down the screen so its controls stay to hand. */
@media (max-width: 63.99rem) {
    .pane {
        overflow: visible;
    }

    .pane-head {
        position: sticky;
        top: 0;
        z-index: 2;
    }
}

/* Monospace, for the pattern itself, the test text and every position count. A pattern is
   an exact string whose characters have to line up under the highlight drawn over them,
   and a proportional font makes that impossible rather than merely awkward. */
.mono {
    font-family: var(--rf-mono);
    font-size: 0.8125rem;
}

/* ── Node colours ─────────────────────────────────────────────────────────────
   The one place the hue names are turned into classes. Everything that draws a piece of a
   pattern - the editor's overlay, the diagram, the explanation, the trace - sets one of
   these, so a literal is the same blue in all four. */
.n-literal { --node: var(--rf-n-literal); }
.n-class { --node: var(--rf-n-class); }
.n-any { --node: var(--rf-n-any); }
.n-group { --node: var(--rf-n-group); }
.n-backref { --node: var(--rf-n-group); }
.n-repeat { --node: var(--rf-n-repeat); }
.n-anchor { --node: var(--rf-n-anchor); }
.n-error { --node: var(--rf-danger); }
.n-sequence,
.n-alternation,
.n-empty { --node: var(--rf-muted); }

/* ── Boot splash ──────────────────────────────────────────────────────────────
   What covers the page until WebAssembly is up and the last pattern is back in the box. It
   lives here, in the stylesheet the document already blocks on, rather than in a scoped
   component file - the markup is in index.html and is on screen long before
   RegExFlex.styles.css means anything.

   Only theme tokens are used, so a first visit gets the right palette from the inline theme
   script in index.html and never flashes the other one. */
.rf-boot {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    place-items: center;
    background: var(--rf-bg);

    /* The shell's typeface, repeated: the splash sits outside `.rf-shell` and so inherits
       nothing from it, and the browser default is a serif. */
    font-family: 'Inter', 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, sans-serif;
}

/* The reveal, in the manner of a stage curtain cut on the diagonal: the top-left and
   bottom-right halves slide apart along the perpendicular of the split. Each triangle
   overshoots the diagonal by a couple of pixels so no seam of workbench shows through the
   join while they are still closed.

   Taken out of flow, because an in-flow child would be a second grid item and `place-items`
   centres each item in its own row - which puts the wordmark half a screen below the
   middle. */
.rf-boot-sheet {
    position: absolute;
    inset: 0;
}

.rf-boot-sheet span {
    position: absolute;
    inset: 0;
    background: var(--rf-bg);
}

.rf-boot-sheet span:first-child {
    clip-path: polygon(0 0, calc(100% + 2px) 0, 0 calc(100% + 2px));
}

.rf-boot-sheet span:last-child {
    clip-path: polygon(100% -2px, 100% 100%, -2px 100%);
}

.rf-boot.is-leaving .rf-boot-sheet span {
    transition: transform 560ms cubic-bezier(0.66, 0, 0.34, 1);
}

.rf-boot.is-leaving .rf-boot-sheet span:first-child {
    transform: translate(-100%, -100%);
}

.rf-boot.is-leaving .rf-boot-sheet span:last-child {
    transform: translate(100%, 100%);
}

.rf-boot-body {
    position: relative;
    display: grid;
    justify-items: center;
    gap: 0;
    padding-bottom: 3vh;
    text-align: center;
}

/* The wordmark goes first and fast, so the curtain opens on an empty stage rather than
   dragging the notice off with it. */
.rf-boot.is-leaving .rf-boot-body {
    opacity: 0;
    transform: scale(0.97);
    transition: opacity 160ms ease, transform 220ms ease;
}

.rf-boot-mark {
    width: 5.5rem;
    height: 5.5rem;
    overflow: visible;
}

/* The mark is the app's own subject: a run of characters with a bracket round the ones a
   pattern has picked out. On the splash the bracket travels along the row, lighting each
   character as it passes and letting it go again - which is exactly what the panel at the
   bottom of the workbench is about to show for real. */
.rf-boot-mark .rf-mark-window {
    animation: rf-boot-sweep-window 2.6s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}

.rf-boot-mark .rf-mark-cell {
    animation: rf-boot-cell 2.6s linear infinite;
}

/* The bracket travels from over the first three cells to over the last three and back. The
   distance is exactly four cells - 16.8 units at this spacing - so it is always sitting over
   three of them rather than between two. */
@keyframes rf-boot-sweep-window {
    0%, 6% { transform: translateX(-8.4px); }
    50%, 56% { transform: translateX(8.4px); }
    100% { transform: translateX(-8.4px); }
}

/* Each cell lights as the bracket arrives over it and goes out as it leaves, which is the
   staggered delay in index.html. The window it is lit for is deliberately wide enough that
   three of them are always on at once. */
@keyframes rf-boot-cell {
    0%, 34%, 100% { fill: var(--rf-faint); }
    10%, 24% { fill: var(--rf-accent-bright); }
}

.rf-boot-name {
    margin: 0.625rem 0 1.5rem;
    font-size: 1.375rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--rf-text);
}

.rf-boot-name span {
    color: var(--rf-accent);
}

.rf-boot-rail {
    position: relative;
    width: 13.75rem;
    max-width: 60vw;
    height: 3px;
    overflow: hidden;
    border-radius: 999px;
    background: var(--rf-chrome-2);
    box-shadow: inset 0 0 0 1px var(--rf-border-soft);
}

.rf-boot-fill {
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--rf-accent-dim), var(--rf-accent-bright));
    transition: width 240ms ease;
}

/* Before the first file is even asked for there is no fraction to draw, and a rail sitting
   at zero is the one thing this screen exists to avoid saying. */
.rf-boot-rail.is-waiting::after {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 35%;
    border-radius: inherit;
    background: linear-gradient(90deg, transparent, var(--rf-accent-dim), transparent);
    animation: rf-boot-slide 1.4s ease-in-out infinite;
}

@keyframes rf-boot-slide {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(386%); }
}

.rf-boot-status {
    margin: 0.875rem 0 0;
    font-size: 0.8125rem;
    color: var(--rf-muted);
}

/* Fixed height, so the counts appearing under the rail do not shift the wordmark up the
   screen a beat after it arrived. */
.rf-boot-detail {
    margin: 0.1875rem 0 0;
    min-height: 1.1rem;
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    color: var(--rf-faint);
}

.rf-boot.is-failed .rf-boot-status {
    color: var(--rf-danger);
}

/* The files that were counted may all have arrived - the runtime's own imports go around
   the count - so the rail can be sitting at a confident 100% under a message saying the
   opposite. It should not look like progress. */
.rf-boot.is-failed .rf-boot-fill {
    background: var(--rf-danger);
    opacity: 0.5;
}

.rf-boot.is-failed .rf-boot-mark .rf-mark-window,
.rf-boot.is-failed .rf-boot-mark .rf-mark-cell {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    .rf-boot-mark .rf-mark-window,
    .rf-boot-mark .rf-mark-cell,
    .rf-boot-rail.is-waiting::after {
        animation: none;
    }

    /* No curtain: the whole cover fades, which is the same information without the movement
       across the viewport. */
    .rf-boot.is-leaving {
        opacity: 0;
        transition: opacity 200ms ease;
    }

    .rf-boot.is-leaving .rf-boot-sheet span,
    .rf-boot.is-leaving .rf-boot-body {
        transition: none;
        transform: none;
        opacity: 1;
    }
}

.blazor-error-boundary {
    background: #b32121;
    padding: 1rem;
    color: white;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}
