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 boxes, the grid cells 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%;
}

/* CronFlex 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 and JwtFlex
   deliberately - the three 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 is violet, and CronFlex is this azure. It is
   also the only part of the palette that must stay clear of the verdict colours
   below, since it appears on chrome that sits next to them. */
:root {
    color-scheme: dark;
    --cf-bg: #0d1015;
    --cf-pane: #15191f;
    --cf-pane-alt: #11151a;
    --cf-chrome: #0a0d11;
    --cf-chrome-2: #12161c;
    --cf-chrome-hover: #1e242d;
    --cf-border: #262f3a;
    --cf-border-soft: #1a212a;
    --cf-text: #dde5ee;
    --cf-muted: #8391a2;
    --cf-faint: #56626f;
    --cf-accent: #4d90f0;
    --cf-accent-bright: #74a9ff;
    --cf-accent-ink: #04162e;
    --cf-accent-dim: rgba(77, 144, 240, 0.45);
    --cf-accent-wash: rgba(77, 144, 240, 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. */
    --cf-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 pane it turns up in. They are
       deliberately only ever used beside a ✓, ! or ✗, because the field hues below
       occupy the same part of the spectrum and hue alone cannot carry a verdict here. */
    --cf-good: #3ecf8e;
    --cf-good-wash: rgba(62, 207, 142, 0.13);
    --cf-warn: #e8a33c;
    --cf-warn-wash: rgba(232, 163, 60, 0.13);
    --cf-danger: #f2777a;
    --cf-danger-wash: rgba(242, 119, 122, 0.13);

    /* One hue per field, in the order the fields are written. These are the whole idea
       of the workbench: the colour of a box in the expression strip is the colour of
       the grid that explains it, of that grid's heading, and of the cells the field
       lights up.

       Six hues, and they steer clear of the accent so that chrome never reads as a
       field. They cannot also steer clear of the three verdict colours - ten distinct
       hues is more than a palette has - so the rule is the other way round: a verdict
       never appears as colour alone, always beside a ✓, ! or ✗. The first is slate
       because seconds are the field most expressions do not have, and a sixth bright
       colour for an absent field would be the loudest thing on a page about the other
       five. */
    --cf-f0: #8b95a6;
    --cf-f1: #35c0d8;
    --cf-f2: #9b7cf6;
    --cf-f3: #e07ac8;
    --cf-f4: #4fc98a;
    --cf-f5: #e8a33c;

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

:root[data-theme="light"] {
    color-scheme: light;
    --cf-bg: #e7ebf0;
    --cf-pane: #ffffff;
    --cf-pane-alt: #f7f9fb;
    --cf-chrome: #f9fbfc;
    --cf-chrome-2: #f1f4f7;
    --cf-chrome-hover: #e4e9ef;
    --cf-border: #ccd5df;
    --cf-border-soft: #dde4eb;
    --cf-text: #18202a;
    --cf-muted: #5a6673;
    --cf-faint: #98a3af;
    --cf-accent: #1d5fc4;
    --cf-accent-bright: #2a6fd6;
    --cf-accent-ink: #ffffff;
    --cf-accent-dim: rgba(29, 95, 196, 0.45);
    --cf-accent-wash: rgba(29, 95, 196, 0.10);

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

    --cf-good: #0f7a52;
    --cf-good-wash: rgba(15, 122, 82, 0.10);
    --cf-warn: #9a5c00;
    --cf-warn-wash: rgba(154, 92, 0, 0.10);
    --cf-danger: #c02f33;
    --cf-danger-wash: rgba(192, 47, 51, 0.09);

    --cf-f0: #5f6b7a;
    --cf-f1: #0d7a8c;
    --cf-f2: #6247d4;
    --cf-f3: #a4348c;
    --cf-f4: #12805a;
    --cf-f5: #8a5712;

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

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
        color-scheme: light;
        --cf-bg: #e7ebf0;
        --cf-pane: #ffffff;
        --cf-pane-alt: #f7f9fb;
        --cf-chrome: #f9fbfc;
        --cf-chrome-2: #f1f4f7;
        --cf-chrome-hover: #e4e9ef;
        --cf-border: #ccd5df;
        --cf-border-soft: #dde4eb;
        --cf-text: #18202a;
        --cf-muted: #5a6673;
        --cf-faint: #98a3af;
        --cf-accent: #1d5fc4;
        --cf-accent-bright: #2a6fd6;
        --cf-accent-ink: #ffffff;
        --cf-accent-dim: rgba(29, 95, 196, 0.45);
        --cf-accent-wash: rgba(29, 95, 196, 0.10);

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

        --cf-good: #0f7a52;
        --cf-good-wash: rgba(15, 122, 82, 0.10);
        --cf-warn: #9a5c00;
        --cf-warn-wash: rgba(154, 92, 0, 0.10);
        --cf-danger: #c02f33;
        --cf-danger-wash: rgba(192, 47, 51, 0.09);

        --cf-f0: #5f6b7a;
        --cf-f1: #0d7a8c;
        --cf-f2: #6247d4;
        --cf-f3: #a4348c;
        --cf-f4: #12805a;
        --cf-f5: #8a5712;

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

body {
    margin: 0;
    background: var(--cf-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(--cf-muted);
    font: inherit;
    font-size: 0.75rem;
    white-space: nowrap;
    cursor: pointer;
}

.btn:hover:not(:disabled) {
    background: var(--cf-chrome-hover);
    border-color: var(--cf-border);
    color: var(--cf-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(--cf-border);
    background: var(--cf-pane);
    color: var(--cf-text);
}

.btn-strong:hover:not(:disabled) {
    border-color: var(--cf-accent-dim);
    background: var(--cf-accent-wash);
    color: var(--cf-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(--cf-border);
    border-radius: 5px;
}

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

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

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

.seg > button.is-on {
    background: var(--cf-accent-wash);
    color: var(--cf-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 field 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(--cf-muted)) 40%, transparent);
    border-radius: 999px;
    background: var(--tone-wash, transparent);
    color: var(--tone, var(--cf-muted));
    font-size: 0.6875rem;
    font-weight: 600;
    white-space: nowrap;
}

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

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

.badge-bad {
    --tone: var(--cf-danger);
    --tone-wash: var(--cf-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(--cf-muted);
    font-size: 0.75rem;
    cursor: pointer;
    user-select: none;
}

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

/* A small dropdown in a header bar, styled to sit beside .btn and .seg rather than to
   look like a form control. */
.pick {
    flex: none;
    padding: 0.125rem 0.25rem;
    border: 1px solid var(--cf-border);
    border-radius: 5px;
    background: transparent;
    color: var(--cf-muted);
    font: inherit;
    font-size: 0.6875rem;
    cursor: pointer;
}

.pick:hover {
    background: var(--cf-chrome-hover);
    color: var(--cf-text);
}

/* 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 three panes - the expression band, the grids and the next runs - 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(--cf-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(--cf-pane-alt);
    border-bottom: 1px solid var(--cf-border-soft);
    box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--pane-hue, var(--cf-accent)) 35%, transparent);
}

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

.pane-meta {
    min-width: 0;
    overflow: hidden;
    color: var(--cf-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: 57.59rem) {
    .pane {
        overflow: visible;
    }

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

/* Monospace, for the expression itself and for every fire time. An expression is an
   exact string whose fields have to line up under each other, and a column of times
   that does not sit on a grid cannot be scanned. */
.mono {
    font-family: 'JetBrains Mono', 'Cascadia Mono', 'Consolas', ui-monospace, monospace;
    font-size: 0.8125rem;
}

/* ── Boot splash ──────────────────────────────────────────────────────────────
   What covers the page until WebAssembly is up and the last expression 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 CronFlex.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. */
.cf-boot {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    place-items: center;
    background: var(--cf-bg);

    /* The shell's typeface, repeated: the splash sits outside `.cf-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. */
.cf-boot-sheet {
    position: absolute;
    inset: 0;
}

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

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

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

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

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

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

.cf-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. */
.cf-boot.is-leaving .cf-boot-body {
    opacity: 0;
    transform: scale(0.97);
    transition: opacity 160ms ease, transform 220ms ease;
}

.cf-boot-mark {
    width: 4.5rem;
    height: 4.5rem;
    overflow: visible;
}

/* The mark is the app's own subject: a grid of slots, three of which are lit. On the
   splash the lit ones travel along it in reading order - a schedule coming round -
   with each dot's turn set by an inline animation-delay in index.html. */
.cf-boot-mark circle {
    animation: cf-boot-slot 2.4s ease-in-out infinite;
}

@keyframes cf-boot-slot {
    0%, 22%, 100% { fill: var(--cf-faint); r: 2; }
    8% { fill: var(--cf-accent-bright); r: 2.6; }
}

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

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

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

.cf-boot-fill {
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--cf-accent-dim), var(--cf-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. */
.cf-boot-rail.is-waiting::after {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 35%;
    border-radius: inherit;
    background: linear-gradient(90deg, transparent, var(--cf-accent-dim), transparent);
    animation: cf-boot-sweep 1.4s ease-in-out infinite;
}

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

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

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

.cf-boot.is-failed .cf-boot-status {
    color: var(--cf-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. */
.cf-boot.is-failed .cf-boot-fill {
    background: var(--cf-danger);
    opacity: 0.5;
}

.cf-boot.is-failed .cf-boot-mark circle {
    animation: none;
}

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

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

    .cf-boot.is-leaving .cf-boot-sheet span,
    .cf-boot.is-leaving .cf-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."
}
