/* ═══════════════════════════════════════════════════════════════════════
   gridlayout — the front-end grid contract
   ───────────────────────────────────────────────────────────────────────
   Sections are placed by editors on a 12-column grid (6 on tablet, 1 on
   mobile) from the Wagtail admin.  Each cell carries a fully resolved
   placement for every breakpoint, written inline by gridlayout/grid.html:

       --c  --s  --r  --rs     desktop  (12 columns)
       --tc --ts --tr --trs    tablet   (6 columns)
       --mc --ms --mr --mrs    mobile   (1 column)

   There is deliberately no reflow logic in this file.  The Icarus original
   emitted only the breakpoints an editor had touched and used var()
   fallbacks to mean "inherit and reflow" — elegant, but it put the reflow
   rule in CSS where the admin canvas could not see it, which is how the
   canvas and the rendered page drifted apart.  gridlayout/layout.py does
   the arithmetic once, in Python, for all three breakpoints; this file
   only positions what it is told to.

   Row heights are a contract, not a measurement.  `grid-auto-rows` and
   `grid-template-rows` use minmax(), so a row_span reserves a minimum and
   real content is always free to make the row taller.

   This app is generic and ships no palette of its own.  Every colour here
   is a --gl-* custom property whose fallback is a non-colour keyword, so
   a project skins the grid by mapping its own design tokens onto them.
   In the prototype that mapping lived in the project's tokens.css; here
   it belongs in core/static/css/core.css and has not been written yet, so
   every --gl-* falls back to `transparent` / `currentColor` / `none` and
   the appearance controls render as plain, unstyled boxes.  That is the
   intended degradation, not a bug — the grid geometry works regardless,
   and the palette lands with the design tokens.
═══════════════════════════════════════════════════════════════════════ */

.gl-grid {
    /* One grid row of reserved height.  Sections almost always exceed it;
       it exists so an editor's height choice means something on a page
       whose content has not been written yet. */
    --gl-row: 90px;
    --gl-gap: 1.5rem;

    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    grid-template-rows: repeat(var(--gl-rows, 1), minmax(var(--gl-row), auto));
    grid-auto-rows: minmax(var(--gl-row), auto);
    gap: var(--gl-gap);
    align-items: stretch;
}

.gl-cell {
    grid-column: var(--c, 1) / span var(--s, 12);
    grid-row: var(--r, auto) / span var(--rs, 1);

    /* Without this a cell containing anything that scrolls horizontally
       refuses to shrink below its content's width and blows the grid out. */
    min-width: 0;

    display: flex;
    flex-direction: column;
}

.gl-cell > * {
    min-width: 0;
}

/* ── Per-breakpoint visibility ────────────────────────────────────────
   Editors can hide a section on phones without deleting it.  Each media
   query re-shows the classes that belong to narrower breakpoints, because
   max-width queries stack: at 500px both the tablet and the mobile query
   are live, so each one has to restate what is visible as well as what is
   not.  Order within each block matters — the hide rule comes last. */

.gl-cell--hide-desktop {
    display: none;
}

/* ── Cell appearance ─────────────────────────────────────────────────── */

/* ── Section backgrounds ──────────────────────────────────────────────
   Painted on a pseudo-element rather than on the cell itself. That is what
   lets a full-width section's colour or photograph span the whole viewport
   while its content stays inside the page's column — the band bleeds, the
   words do not.

   `isolation: isolate` keeps the z-index: -1 layer inside the cell's own
   stacking context, so it sits behind the section's content and in front
   of the page, rather than disappearing behind the body. */

.gl-cell--bg-tint,
.gl-cell--bg-primary,
.gl-cell--bg-secondary,
.gl-cell--bg-accent,
.gl-cell--bg-dark,
.gl-cell--has-bg-image {
    position: relative;
    isolation: isolate;
    border-radius: var(--gl-radius, 0);
}

.gl-cell--bg-tint::before,
.gl-cell--bg-primary::before,
.gl-cell--bg-secondary::before,
.gl-cell--bg-accent::before,
.gl-cell--bg-dark::before,
.gl-cell--has-bg-image::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background-color: var(--gl-cell-bg, transparent);
    background-position: center;
    background-size: cover;
}

.gl-cell--bg-tint      { --gl-cell-bg: var(--gl-bg-tint, transparent); }
.gl-cell--bg-primary   { --gl-cell-bg: var(--gl-bg-primary, transparent); color: var(--gl-on-primary, inherit); }
.gl-cell--bg-secondary { --gl-cell-bg: var(--gl-bg-secondary, transparent); color: var(--gl-on-secondary, inherit); }
.gl-cell--bg-accent    { --gl-cell-bg: var(--gl-bg-accent, transparent); }
.gl-cell--bg-dark      { --gl-cell-bg: var(--gl-bg-dark, transparent); color: var(--gl-on-dark, inherit); }

/* A photograph always gets a scrim over it. Text has to hold 4.5:1 against
   the *lightest* pixel behind it, and the lightest pixel of an editor's
   photograph is unknowable when this rule is written — so the contrast is
   guaranteed by the overlay rather than hoped for. */
.gl-cell--has-bg-image::before {
    background-image:
        linear-gradient(var(--gl-bg-scrim, transparent), var(--gl-bg-scrim, transparent)),
        var(--gl-bg-image, none);
}
.gl-cell--has-bg-image { color: var(--gl-on-image, inherit); }

/* ── Full-bleed bands ─────────────────────────────────────────────────
   The background escapes the page column; the content does not. Only
   applied to sections that span the whole grid — layout.py refuses it for
   anything narrower, because the band is centred on the cell and a
   half-width cell would throw it off to one side.

   100vw includes the scrollbar, so this can overshoot by a scrollbar's
   width. `overflow-x: clip` on the page body absorbs that without creating
   a scroll container (which `hidden` would, breaking position: sticky). */
.gl-cell--bleed-full::before {
    inset-inline: calc(50% - 50vw);
    /* Half the grid gap above and below, so two stacked bands of the same
       colour meet as one continuous band instead of showing a hairline of
       page through the gutter between them. A lone band just runs a little
       into the gutter, which reads as intentional. */
    inset-block: calc(var(--gl-gap) / -2);
}

.gl-cell--bordered {
    border: 1px solid var(--gl-border, currentColor);
    border-radius: var(--gl-radius, 0);
}

.gl-cell--panel {
    background: var(--gl-panel-bg, transparent);
    border-radius: var(--gl-radius, 0);
    box-shadow: var(--gl-panel-shadow, none);
}

/* ── Text alignment ───────────────────────────────────────────────────
   Set on the cell, so it cascades into whatever the section puts inside
   itself — including sections that did not exist when this was written.
   Logical values (`start`/`end`, not `left`/`right`) so a right-to-left
   locale added later needs no new rules. */

.gl-cell--align-start   { text-align: start; }
.gl-cell--align-center  { text-align: center; }
.gl-cell--align-end     { text-align: end; }

.gl-cell--align-justify {
    text-align: justify;
    /* Justified text with no hyphenation opens rivers of white space down
       the column, which is a readability problem before it is an aesthetic
       one. `hyphens` needs the element's language to be known — base.html
       sets `lang` on <html> (statically for now; from the active locale
       once this site is translated), so it is. */
    hyphens: auto;
    -webkit-hyphens: auto;
}

/* Simple Law (D102): the project's sections.css narrows this to body text
   — headings, labels, titles and buttons fall back to start — because
   justifying the whole cell stretched headlines and card titles. Left
   as it is here, so the app stays usable without that stylesheet. */

/* ── Where the section sits in a taller cell ──────────────────────────
   .gl-cell is a column flex container, so justify-content is the vertical
   axis. `stretch` is the default and is what every layout did before this
   existed, so an older stored layout is unaffected.

   The children need their height released too: a section that sets
   `height: 100%` to fill its cell will otherwise go on filling it and
   there will be nothing left to align. */

.gl-cell--valign-stretch { justify-content: stretch; }
.gl-cell--valign-start   { justify-content: flex-start; }
.gl-cell--valign-center  { justify-content: center; }
.gl-cell--valign-end     { justify-content: flex-end; }

/* Doubled class on purpose. grid.css loads before the project's own
   stylesheet, and a section that sets `height: 100%` matches at the same
   specificity — so a single class here would lose on source order and the
   alignment would silently do nothing. */
.gl-cell.gl-cell--valign-start > *,
.gl-cell.gl-cell--valign-center > *,
.gl-cell.gl-cell--valign-end > * {
    height: auto;
    flex: 0 0 auto;
}

.gl-cell--pad-none { padding: 0; }
.gl-cell--pad-sm   { padding: var(--gl-pad-sm, .5rem); }
.gl-cell--pad-md   { padding: var(--gl-pad-md, 1rem) 0; }
.gl-cell--pad-lg   { padding: var(--gl-pad-lg, 2rem) var(--gl-pad-md, 1rem); }

/* A cell with a background or a border needs horizontal padding too,
   otherwise its content sits flush against its own edge. */
.gl-cell--bordered.gl-cell--pad-md,
.gl-cell--panel.gl-cell--pad-md,
.gl-cell--bg-tint.gl-cell--pad-md,
.gl-cell--bg-primary.gl-cell--pad-md,
.gl-cell--bg-secondary.gl-cell--pad-md,
.gl-cell--bg-accent.gl-cell--pad-md,
.gl-cell--bg-dark.gl-cell--pad-md,
.gl-cell--has-bg-image.gl-cell--pad-md {
    padding: var(--gl-pad-md, 1rem);
}

/* ── Sections that scroll inside their cell ───────────────────────────
   Opted into by the block itself (`class Meta: gl_scrolls = True`), for
   list-shaped sections — a notifications column, a long link list — that
   would otherwise dictate the height of every section beside them.

   Taking the section out of flow is the trick.  A grid row sized `auto`
   is as tall as its tallest item, so a twelve-item list *becomes* the row
   height.  Absolutely positioning the section means it contributes
   nothing to that calculation: the row is sized by its other occupants,
   the cell stretches to the row, and `inset: 0` gives the section a
   definite height to scroll within.

   That definite height is the part that matters.  `max-height: 100%`
   looks equivalent and is not — a percentage height against a parent with
   no definite height resolves to `none`, so the cap silently disappears.

   Only above the tablet breakpoint: below it the grid is one or two
   columns and there is nothing beside the section to match, so a scroll
   region would just be a second scrollbar on a phone. */
@media (min-width: 1025px) {
    .gl-cell--scrolls {
        position: relative;
    }

    .gl-cell--scrolls > * {
        position: absolute;
        inset: 0;
        overflow-y: auto;
        max-height: none;
    }
}

/* ── Entrance animation (Simple Law fork, D97) ───────────────────────────
   Chosen per section in the canvas, separately for headings, body text,
   images and buttons; played once, when the section first scrolls into
   view, by gridlayout/js/reveal.js. layout.py marks the cell with
   `gl-cell--anim` and each group's settings as --gl-anim-<group>. These
   rules only describe the start and end states.

   Three stages:
     1. Before reveal.js has prepared a cell, its content is hidden
        outright — but only under `html.gl-anim-on`, which reveal.js sets
        from <head> and only when it can animate. No script, no hiding.
     2. Armed: each moving piece is a `.gl-anim-unit` with a `gl-fx-<effect>`
        class, held at its start state and carrying its own delay and
        duration (--gl-anim-unit-delay, --gl-anim-unit-duration). Anything
        no group animates is simply shown.
     3. In: the units run to their end state. Afterwards reveal.js removes
        every trace of this, so the finished page is the page as served.

   Opacity and transform do nearly all of it, the two properties a browser
   can animate without laying the page out again; blur adds a filter and
   wipe a clip-path, on the units that use them only. The cell — and with
   it the background painted on its ::before — never moves. */

.gl-anim-on .gl-cell--anim:not(.gl-anim-armed):not(.gl-anim-done) > * {
    opacity: 0;
}

.gl-fx-fade        { --gl-anim-from: none; }
.gl-fx-fade-up     { --gl-anim-from: translateY(1.5rem); }
.gl-fx-slide-left  { --gl-anim-from: translateX(-2.5rem); }
.gl-fx-slide-right { --gl-anim-from: translateX(2.5rem); }
.gl-fx-zoom        { --gl-anim-from: scale(0.92); }
/* A word rises a little as it fades in; a letter only fades, which with a
   short duration reads as typing. */
.gl-fx-words       { --gl-anim-from: translateY(0.4em); }
.gl-fx-letters     { --gl-anim-from: none; }
/* Pictures only. Roll: in from the left, turning as it comes. */
.gl-fx-roll        { --gl-anim-from: translateX(-4rem) rotate(-12deg) scale(0.9); }
.gl-fx-blur        { --gl-anim-from: scale(1.04); }
.gl-fx-wipe        { --gl-anim-from: none; }

.gl-cell--anim.gl-anim-armed .gl-anim-unit {
    opacity: 0;
    transform: var(--gl-anim-from, none);
}

/* Blur: sharpens as it fades in. Wipe: fully opaque, uncovered from the
   left edge across. */
.gl-cell--anim.gl-anim-armed .gl-anim-unit.gl-fx-blur {
    filter: blur(12px);
}

.gl-cell--anim.gl-anim-armed .gl-anim-unit.gl-fx-wipe {
    opacity: 1;
    clip-path: inset(0 100% 0 0);
}

/* The transition belongs to this state only. Declared on the armed state
   as well, arming would itself be animated: the pieces were visible a
   moment earlier (only their parent was hidden), so they would be seen
   fading *out* before fading in. */
.gl-cell--anim.gl-anim-armed.gl-anim-in .gl-anim-unit {
    opacity: 1;
    transform: none;
    transition-property: opacity, transform, filter, clip-path;
    transition-duration: var(--gl-anim-unit-duration, 0.7s);
    transition-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1);
    transition-delay: var(--gl-anim-unit-delay, 0s);
}

.gl-cell--anim.gl-anim-armed.gl-anim-in .gl-anim-unit.gl-fx-blur {
    filter: blur(0);
}

.gl-cell--anim.gl-anim-armed.gl-anim-in .gl-anim-unit.gl-fx-wipe {
    clip-path: inset(0 0 0 0);
}

/* ── Exit animation (Simple Law fork, D99) ──────────────────────────────
   The way out, as a section leaves the screen. reveal.js marks the moving
   pieces exactly as for the entrance (`.gl-anim-unit`, `gl-fx-<effect>`,
   their own delay and duration) and puts `gl-anim-exiting` on the cell,
   which holds them where they are — visible; `gl-anim-out` then sends them
   to the exit's end state. Taking `gl-anim-out` off runs it backwards,
   which is how a section with no entrance comes back.

   Not scoped to `.gl-cell--anim`: a section with only an exit carries
   `gl-cell--anim-exit` instead, because the former hides content before
   its first entrance.

   The effect names are the entrance's, meaning the mirror movement, so
   each needs its own end point: */
.gl-fx-fade        { --gl-exit-to: none; }
.gl-fx-fade-up     { --gl-exit-to: translateY(-1.5rem); }
.gl-fx-slide-left  { --gl-exit-to: translateX(-2.5rem); }
.gl-fx-slide-right { --gl-exit-to: translateX(2.5rem); }
.gl-fx-zoom        { --gl-exit-to: scale(0.92); }
.gl-fx-words       { --gl-exit-to: translateY(-0.4em); }
.gl-fx-letters     { --gl-exit-to: none; }
/* Pictures only. Roll: away to the right, turning as it goes. */
.gl-fx-roll        { --gl-exit-to: translateX(4rem) rotate(12deg) scale(0.9); }
.gl-fx-blur        { --gl-exit-to: scale(1.04); }
.gl-fx-wipe        { --gl-exit-to: none; }

/* Declared on the held state as well as the end state, unlike the
   entrance: here the held state *is* the visible one, so nothing jumps
   when the class goes on, and it is what lets the exit run backwards.
   Accelerating rather than easing out — things leave by speeding up. */
.gl-anim-exiting .gl-anim-unit {
    transition-property: opacity, transform, filter, clip-path;
    transition-duration: var(--gl-anim-unit-duration, 0.7s);
    transition-timing-function: cubic-bezier(0.5, 0, 0.75, 0);
    transition-delay: var(--gl-anim-unit-delay, 0s);
}

/* Blur and wipe need their visible state spelled out, so there is
   something for the transition to run from. */
.gl-anim-exiting .gl-anim-unit.gl-fx-blur {
    filter: blur(0);
}

.gl-anim-exiting .gl-anim-unit.gl-fx-wipe {
    clip-path: inset(0 0 0 0);
}

.gl-anim-exiting.gl-anim-out .gl-anim-unit {
    opacity: 0;
    transform: var(--gl-exit-to, none);
}

.gl-anim-exiting.gl-anim-out .gl-anim-unit.gl-fx-blur {
    filter: blur(12px);
}

/* Wipe: stays opaque and is uncovered away to the right. */
.gl-anim-exiting.gl-anim-out .gl-anim-unit.gl-fx-wipe {
    opacity: 1;
    clip-path: inset(0 0 0 100%);
}

/* ── A frame that animates its own parts (Simple Law fork, D100) ────────
   The Images effect "frame" picks out elements marked data-gl-frame
   instead of pictures. The marked element itself neither fades nor moves
   — its box is the frame's place on the page — so the generic start and
   end states above are cancelled for it; the project's stylesheet moves
   what is inside it, using the same states and timings. */
.gl-fx-frame        { --gl-anim-from: none; }
.gl-fx-frame        { --gl-exit-to: none; }
/* "frame-turn" (patch 07): the same frame, the picture turning with it. */
.gl-fx-frame-turn   { --gl-anim-from: none; }
.gl-fx-frame-turn   { --gl-exit-to: none; }

.gl-cell--anim.gl-anim-armed .gl-anim-unit.gl-fx-frame,
.gl-anim-exiting.gl-anim-out .gl-anim-unit.gl-fx-frame,
.gl-cell--anim.gl-anim-armed .gl-anim-unit.gl-fx-frame-turn,
.gl-anim-exiting.gl-anim-out .gl-anim-unit.gl-fx-frame-turn {
    opacity: 1;
    transform: none;
}

/* Word and letter spans exist only while they animate. A transform needs
   a box, hence inline-block; the box round a word of letters must not
   break between them, hence nowrap. */
.gl-anim-word,
.gl-anim-letter {
    display: inline-block;
}

.gl-anim-word-box {
    display: inline-block;
    white-space: nowrap;
}

/* The intact text offered to screen readers while the cut-up copy is on
   screen. Visually hidden, not display: none, so it is still read. */
.gl-anim-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Belt and braces. reveal.js already does nothing for a visitor who has
   asked for less motion, and a printed page has no scroll to wait for. */
@media (prefers-reduced-motion: reduce) {
    .gl-cell--anim > *,
    .gl-cell--anim .gl-anim-unit,
    .gl-cell--anim-exit .gl-anim-unit {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
        clip-path: none !important;
        transition: none !important;
    }
}

@media print {
    .gl-cell--anim > *,
    .gl-cell--anim .gl-anim-unit,
    .gl-cell--anim-exit .gl-anim-unit {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
        clip-path: none !important;
    }
}

/* ── Tablet ───────────────────────────────────────────────────────────── */

@media (max-width: 1024px) {
    .gl-grid {
        --gl-row: 72px;
        --gl-gap: 1.25rem;
        grid-template-columns: repeat(6, minmax(0, 1fr));
        grid-template-rows: repeat(var(--gl-t-rows, 1), minmax(var(--gl-row), auto));
    }

    .gl-cell {
        grid-column: var(--tc, 1) / span var(--ts, 6);
        grid-row: var(--tr, auto) / span var(--trs, 1);
    }

    .gl-cell--hide-desktop { display: flex; }
    .gl-cell--hide-tablet  { display: none; }
}

/* ── Mobile ───────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
    .gl-grid {
        --gl-row: 56px;
        --gl-gap: 1rem;
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: repeat(var(--gl-m-rows, 1), minmax(var(--gl-row), auto));
    }

    .gl-cell {
        grid-column: var(--mc, 1) / span var(--ms, 1);
        grid-row: var(--mr, auto) / span var(--mrs, 1);
    }

    .gl-cell--hide-desktop,
    .gl-cell--hide-tablet  { display: flex; }
    .gl-cell--hide-mobile  { display: none; }

    /* Justified text in a column this narrow produces word spacing wide
       enough to break reading, and WCAG 1.4.8 advises against it outright.
       The editor's choice is honoured everywhere it is readable and quietly
       relaxed where it is not — the alternative is asking them to set the
       same thing three times and hope they get it right. */
    /* Revised (Simple Law fork, D62/D102): no longer relaxed. Justify now
       means justified on phones too, and the editor chooses Left where a
       phone's short lines look awkward; the canvas's note under the
       control says so. The project's own justify rules (sections.css)
       were changed the same way, so the two still agree. */
}

/* Editors who arrange by dragging cannot see focus order.  A visible
   outline on the section a keyboard user has tabbed into is the only cue
   that the grid has not scrambled it. */
.gl-cell:focus-within {
    outline: none;
}
