/* ==========================================================================
   DGS Report Card - application screens.

   The printed card has its own stylesheet (card.css). This one dresses the
   rooms a teacher works in: sign-in, class lists, the card editor, admin.

   Cache-busting is by filemtime() in rcAsset(), so there is no ?v= token to
   remember to bump when this file changes.
   ========================================================================== */

:root {
    /*
     * skill-ed.in's own tokens, so the screens a teacher works in and the card
     * they produce come from one palette. The first version of this was deep
     * green and gold, which belonged to a different school entirely.
     */
    --coral:  #f8732e;  --coral-d: #e85c1a;
    --sky:    #2dbeec;  --sky-d:   #179ec9;
    --sun:    #ffc02e;  --sun-d:   #f3a712;
    --leaf:   #8cc63e;  --leaf-d:  #6fae26;
    --berry:  #f0568c;  --berry-d: #d63d73;
    --grape:  #9b6dde;  --grape-d: #7d4fc4;

    --ink:       #3a2f55;   /* friendly deep purple instead of black */
    --ink-soft:  #6c6286;
    /*
     * Darkened from #9a92ad, which measured 2.96:1 against plain white - below
     * the 4.5:1 floor before a single pane of glass was involved. It carries
     * the small print everywhere: table headings, row subtitles, hints,
     * breadcrumbs. This reads at 4.93:1 on white and still sits back from the
     * body text.
     */
    --ink-faint: #706887;
    --paper:     #f7f3fb;
    --card:      #ffffff;
    --cream:     #fff8ec;
    --line:      #e6e0f0;
    --line-soft: #f1edf7;
    /* Secondary text when it sits on translucent glass rather than
       solid white - the normal --ink-soft is too light there. */
    --glass-muted: #55496f;

    /* Pale washes, one per accent, for chips and panels. */
    --coral-pale: #ffe9dc;
    --sky-pale:   #e2f6fd;
    --sun-pale:   #fff4d9;
    --leaf-pale:  #eef8e0;
    --berry-pale: #ffe4ee;
    --grape-pale: #f0e8fd;
    /* The bright palette is for fills. Writing needs darker ink, or it fails
       against the pale tint behind it: leaf was 2.47:1, berry 3.68:1. */
    --sun-ink:    #7d5710;
    --leaf-ink:   #4e7c18;
    --berry-ink:  #c7255f;

    /* The four growth levels, identical to card.css so what a teacher picks on
       screen is the colour that prints. */
    --g1: var(--sky);
    --g2: var(--leaf);
    --g3: var(--sun);
    --g4: var(--coral);

    /*
     * One spacing scale, on a 4px grid. Before this the markup carried ten
     * different top margins by hand - 1rem, 1.5rem, 1.2rem, .9rem, .8rem,
     * .6rem, .5rem - so no two panels breathed the same way. Everything below
     * uses these, and the .gap-* utilities exist so a template never has to
     * write a margin inline again.
     */
    --s-1: .25rem;
    --s-2: .5rem;
    --s-3: .75rem;
    --s-4: 1rem;
    --s-5: 1.25rem;
    --s-6: 1.5rem;
    --s-7: 2rem;

    --radius:   14px;
    --radius-l: 22px;
    --shadow:   0 8px 20px rgba(58, 47, 85, .10);
    --shadow-l: 0 18px 40px rgba(58, 47, 85, .14);
    --font:  'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --serif: 'Baloo 2', 'Fredoka', Georgia, "Iowan Old Style", "Palatino Linotype", Palatino, serif;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font: 15px/1.6 var(--font);
    color: var(--ink);
    display: flex;
    flex-direction: column;
    min-height: 100vh;

    /*
     * Glass needs something behind it, but a working screen is not a login
     * screen: these washes are at .14 where the sign-in card sits over .55,
     * because this page carries tables and small print rather than four
     * fields. Enough colour for the panels to pick up, not enough to read
     * through them.
     */
    background:
        radial-gradient(42rem 42rem at 6% -6%, rgba(248, 115, 46, .30), transparent 62%),
        radial-gradient(40rem 40rem at 96% 2%, rgba(45, 190, 236, .30), transparent 62%),
        radial-gradient(46rem 46rem at 88% 96%, rgba(240, 86, 140, .26), transparent 62%),
        radial-gradient(36rem 36rem at 4% 92%, rgba(255, 192, 46, .24), transparent 62%),
        var(--paper);
    background-attachment: fixed;
}

a { color: var(--coral-d); }
a:hover { color: var(--coral); }

h1, h2, h3 { line-height: 1.25; margin: 0 0 .4em; font-weight: 800; font-family: var(--serif); }
h1 { font-size: 1.5rem; letter-spacing: -.01em; }
h2 { font-size: 1.15rem; }
h3 { font-size: 1rem; }

/* --------------------------------------------------------------------------
   Top bar
   -------------------------------------------------------------------------- */

/*
 * Dark glass, and sticky, because the two go together: a translucent bar over
 * a page that never moves is just a bar with the wrong opacity. The point is
 * watching the class tiles slide under it.
 */
.topbar {
    position: sticky;
    top: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 1.5rem;
    padding: var(--s-3) var(--s-5);
    color: #fff;
    flex-wrap: wrap;

    background: rgba(48, 39, 72, .78);
    -webkit-backdrop-filter: blur(22px) saturate(180%);
    backdrop-filter: blur(22px) saturate(180%);
    border-bottom: 1px solid rgba(255, 255, 255, .10);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, .12),
        0 6px 24px rgba(35, 26, 58, .18);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .topbar { background: var(--ink); }
}

.brand { display: flex; flex-direction: column; gap: .15rem; text-decoration: none; color: #fff; line-height: 1.15; }
/* The logo is multicolour on transparent, so it sits happily on the purple
   bar without needing a reversed version. */
.brand-logo { display: block; height: 26px; width: auto; }
.brand-sub { font-size: .68rem; text-transform: uppercase; letter-spacing: .12em; opacity: .75; }

.mainnav { display: flex; gap: .25rem; flex: 1; flex-wrap: wrap; }
.mainnav a {
    color: rgba(255, 255, 255, .82);
    text-decoration: none;
    padding: .35rem .7rem;
    border-radius: 999px;
    font-size: .92rem;
    white-space: nowrap;
}
.mainnav a:hover { background: rgba(255, 255, 255, .12); color: #fff; }
.mainnav a.on { background: rgba(255, 255, 255, .18); color: #fff; font-weight: 600; }

.whoami { display: flex; align-items: baseline; gap: .55rem; font-size: .85rem; }
.who-name { font-weight: 600; }
.who-role { opacity: .68; font-size: .76rem; text-transform: uppercase; letter-spacing: .07em; }
.signout { color: rgba(255, 255, 255, .82); text-decoration: none; border-bottom: 1px solid rgba(255, 255, 255, .35); }
.signout:hover { color: #fff; }

/* --------------------------------------------------------------------------
   Page frame
   -------------------------------------------------------------------------- */

/*
 * min-width:0 is doing real work here. body is a flex column and .wrap is a
 * flex item, and a flex item defaults to min-width:auto - it refuses to shrink
 * below its content. The rating table has a 30rem minimum, so .wrap grew to
 * fit it and dragged the whole page sideways on a phone, even though the table
 * had its own scroll box. Zero lets it shrink and lets the box do its job.
 */
.wrap { flex: 1; min-width: 0; width: min(1120px, 100% - 2.5rem); margin: 1.75rem auto; }
.wrap.narrow { width: min(640px, 100% - 2.5rem); }

.pagefoot {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 1.1rem 1.5rem;
    border-top: 1px solid var(--line);
    color: var(--ink-faint);
    font-size: .82rem;
}
.foot-note { font-style: italic; }

.pagehead {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--s-4);
    flex-wrap: wrap;
    margin-bottom: var(--s-5);
}
.pagehead p { margin: .15rem 0 0; color: var(--ink-soft); font-size: .92rem; }

.crumb { font-size: .82rem; color: var(--ink-faint); margin-bottom: .3rem; }
.crumb a { color: var(--ink-faint); }

/* --------------------------------------------------------------------------
   Panels, tables
   -------------------------------------------------------------------------- */

/*
 * Light glass. .82 rather than the sign-in card's .80 because a panel here can
 * be a table of forty children, and every extra point of opacity is contrast
 * the small print keeps. The inset top line is the lit rim of the pane.
 */
.panel {
    background: rgba(255, 255, 255, .82);
    -webkit-backdrop-filter: blur(20px) saturate(170%);
    backdrop-filter: blur(20px) saturate(170%);
    border: 1px solid rgba(255, 255, 255, .70);
    border-radius: var(--radius-l);
    box-shadow:
        0 10px 30px rgba(58, 47, 85, .10),
        inset 0 1px 0 rgba(255, 255, 255, .90);
    padding: var(--s-5);
    margin-bottom: var(--s-5);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .panel { background: rgba(255, 255, 255, .97); }
}
.panel > :last-child { margin-bottom: 0; }
.panel-head { display: flex; justify-content: space-between; align-items: center; gap: var(--s-4); flex-wrap: wrap; margin-bottom: var(--s-4); }
.panel-head h2 { margin: 0; }
.panel-note { color: var(--ink-soft); font-size: .88rem; margin: calc(var(--s-1) * -1) 0 var(--s-4); }

table.grid { width: 100%; border-collapse: collapse; font-size: .93rem; }
table.grid th, table.grid td { padding: var(--s-2) var(--s-2); text-align: left; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
table.grid th {
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--ink-faint);
    font-weight: 700;
    border-bottom: 1px solid var(--line);
}
table.grid tbody tr:hover { background: var(--grape-pale); }
table.grid td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.grid .rowname { font-weight: 600; }
table.grid .rowsub { display: block; color: var(--ink-faint); font-size: .82rem; font-weight: 400; }

.empty { padding: var(--s-7) var(--s-4); text-align: center; color: var(--ink-faint); }
.empty strong { display: block; color: var(--ink-soft); font-size: 1.02rem; margin-bottom: .25rem; }

/* Class tiles on the dashboard */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--s-4); }
.tile {
    display: block;
    background: rgba(255, 255, 255, .78);
    -webkit-backdrop-filter: blur(18px) saturate(170%);
    backdrop-filter: blur(18px) saturate(170%);
    border: 1px solid rgba(255, 255, 255, .70);
    border-radius: var(--radius-l);
    box-shadow:
        0 10px 28px rgba(58, 47, 85, .10),
        inset 0 1px 0 rgba(255, 255, 255, .90);
    padding: 1.1rem 1.2rem;
    text-decoration: none;
    color: inherit;
    transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.tile:hover {
    transform: translateY(-3px);
    background: rgba(255, 255, 255, .90);
    box-shadow:
        0 18px 40px rgba(58, 47, 85, .16),
        inset 0 1px 0 rgba(255, 255, 255, .95);
    color: inherit;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .tile { background: rgba(255, 255, 255, .97); }
}
@media (prefers-reduced-motion: reduce) {
    .tile, .tile:hover { transform: none; transition: none; }
}
.tile-name { font-size: 1.12rem; font-weight: 650; }
.tile-house { color: var(--berry); font-size: .84rem; font-weight: 700; letter-spacing: .02em; }
.tile-meta { margin-top: .7rem; color: var(--ink-soft); font-size: .86rem; display: flex; gap: .8rem; flex-wrap: wrap; }

/* --------------------------------------------------------------------------
   Buttons, chips, progress
   -------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .5rem .95rem;
    border-radius: 999px;
    border: 2px solid var(--coral);
    background: var(--coral);
    color: #fff;
    font: inherit;
    font-size: .9rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}
.btn:hover { background: var(--coral-d); border-color: var(--coral-d); color: #fff; }
.btn-ghost { background: transparent; color: var(--coral-d); }
.btn-ghost:hover { background: var(--coral-pale); color: var(--coral-d); }
/* Secondary buttons are glass too, or they sit on a glass panel looking like
   stickers. */
.btn-plain {
    background: rgba(255, 255, 255, .72);
    -webkit-backdrop-filter: blur(12px) saturate(150%);
    backdrop-filter: blur(12px) saturate(150%);
    color: var(--ink-soft);
    border-color: rgba(58, 47, 85, .14);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .70);
}
.btn-plain:hover { background: var(--grape-pale); color: var(--ink); border-color: var(--grape); }
.btn-danger { background: var(--berry-d); border-color: var(--berry-d); }
.btn-danger:hover { background: #b8305f; border-color: #b8305f; }
.btn-sm { padding: .3rem .7rem; font-size: .82rem; }
.btn[disabled], .btn:disabled { opacity: .5; cursor: not-allowed; }

.btnrow { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }

.chip {
    display: inline-block;
    padding: .18rem .65rem;
    border-radius: 999px;
    font-size: .74rem;
    font-weight: 800;
    letter-spacing: .03em;
    text-transform: uppercase;
    white-space: nowrap;
    /* A hairline of its own colour and a lit top edge, so a chip reads as a
       tinted pane on the panel rather than a sticker pressed onto it. */
    border: 1px solid rgba(58, 47, 85, .08);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .60);
}
.chip-draft     { background: var(--line-soft); color: var(--ink-soft); }
.chip-submitted { background: var(--sun-pale);   color: var(--sun-ink); }
.chip-published { background: var(--leaf-pale);  color: var(--leaf-ink); }

.prog { display: inline-flex; align-items: center; gap: .5rem; }
.prog-bar { display: block; width: 84px; height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.prog-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--g3); }
.prog-full .prog-bar > span { background: var(--leaf); }
.prog-half .prog-bar > span { background: var(--sun); }
.prog-low  .prog-bar > span { background: var(--coral); }
.prog-num { font-size: .8rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; min-width: 2.6em; }

.flash {
    padding: var(--s-3) var(--s-4);
    border-radius: var(--radius);
    margin-bottom: var(--s-4);
    font-size: .93rem;
    border: 1px solid transparent;
    -webkit-backdrop-filter: blur(14px) saturate(160%);
    backdrop-filter: blur(14px) saturate(160%);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55);
}
.flash-ok   { background: var(--leaf-pale);  border-color: #cfe6ad; color: var(--leaf-ink); }
.flash-warn { background: var(--sun-pale);   border-color: #f4dda2; color: var(--sun-ink); }
.flash-err  { background: var(--berry-pale); border-color: #f8c4d8; color: var(--berry-ink); }

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

label { display: block; font-weight: 600; font-size: .88rem; margin-bottom: .28rem; }
.hint { font-weight: 400; color: var(--ink-faint); font-size: .82rem; display: block; margin-top: .18rem; }

/*
 * Wells cut into the panel rather than tiles laid on it: a touch translucent,
 * with a hairline inset shadow at the top so the edge catches light.
 *
 * .72 sounds thin, but translucency compounds here and it lands almost white:
 * this sits inside a .82 panel which itself sits on a pale wash, so what a
 * teacher types comes out at better than 11:1. Measured, not assumed.
 */
input[type=text], input[type=email], input[type=password], input[type=date],
input[type=number], input[type=url], select, textarea {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid rgba(58, 47, 85, .13);
    border-radius: var(--radius);
    background: rgba(255, 255, 255, .72);
    box-shadow: inset 0 1px 2px rgba(58, 47, 85, .05);
    font: inherit;
    color: var(--ink);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    input[type=text], input[type=email], input[type=password], input[type=date],
    input[type=number], input[type=url], select, textarea { background: #fff; }
}
/*
 * One height for every single-line control, stated rather than inherited.
 *
 * A date input carries a picker icon and renders about 2px taller than a text
 * input. Invisible on its own; obvious the moment the two sit in a row that is
 * meant to line up - which is exactly what happened when Date of birth became
 * a calendar and stopped matching Name and My class teacher beside it.
 * min-height did not fix it, because 2px taller was still above the minimum.
 */
input[type=text], input[type=email], input[type=password], input[type=date],
input[type=number], input[type=url], select { height: 2.7rem; }

textarea { line-height: 1.6; resize: vertical; min-height: 6.5rem; font-family: var(--font); }
input:focus, select:focus, textarea:focus {
    outline: 2px solid var(--grape);
    outline-offset: 1px;
    border-color: var(--grape);
    background: rgba(255, 255, 255, .97);
    box-shadow: inset 0 1px 2px rgba(58, 47, 85, .04);
}
input[readonly], input:disabled, textarea:disabled { background: var(--line-soft); color: var(--ink-soft); }
input:disabled, textarea:disabled { cursor: not-allowed; opacity: .8; }

.field { margin-bottom: var(--s-4); }
.fields { display: grid; gap: var(--s-4); grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.fields.two { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/*
 * Line the controls up across a row, whatever height the labels above them
 * happen to be.
 *
 * "Name" carries a hint - "From the school roll" - and "Date of birth" and
 * "My class teacher" do not. Content flowing top-down inside each cell put the
 * Name box a whole line lower than the two beside it, so a row that should
 * read as one row read as a staircase.
 *
 * The grid already stretches every cell to the tallest, so making the cell a
 * flex column and giving the control margin-top:auto drops all three onto the
 * same baseline. Scoped to controls inside .fields: a lone .field has no spare
 * height, so the auto margin does nothing there.
 */
.fields > .field { display: flex; flex-direction: column; margin-bottom: 0; }
.fields > .field > input,
.fields > .field > select,
.fields > .field > .inline-pair { margin-top: auto; }

.check { display: flex; align-items: flex-start; gap: var(--s-2); font-weight: 400; }
.check input { width: auto; margin-top: .28rem; }

/* --------------------------------------------------------------------------
   Spacing utilities

   Deliberately few. Three sizes covered every gap the templates had been
   setting by hand - ten different top margins across nine files, so no two
   panels breathed the same way. A short list is what stops the next person
   inventing an eleventh.
   -------------------------------------------------------------------------- */

.gap-sm   { margin-top: var(--s-2); }
.gap-md   { margin-top: var(--s-4); }
.gap-lg   { margin-top: var(--s-6); }
.gap-none { margin: 0; }
.flush    { margin-bottom: 0; }

/* The table rule below is more specific than a bare class, so the utility has
   to say table cell out loud or a right-aligned action column stays left. */
.align-right, table.grid th.align-right, table.grid td.align-right { text-align: right; }
/* Only bites outside a flex row: a flex child is blockified whatever this
   says, which is why the inline style it replaced did nothing in .btnrow
   either. It is the hint spans that need it. */
.inline-form { display: inline; }
.row-end     { justify-content: flex-end; }
/* Rows that are off the roll, or template rows that have been retired.
   These were dimmed with opacity, which dropped their text to 2.79:1 - and
   they are exactly the rows an admin has to read in order to put them back.
   The state is carried by a tint and by the wording beside the name. */
tr.is-off > td            { background: var(--line-soft); }
tr.is-off > td,
tr.is-off .rowname        { color: var(--ink-soft); font-weight: 500; }

/* A label sitting beside its control on one line, rather than above it. */
.label-inline { margin: 0 var(--s-2) 0 0; }
/* Prose that belongs to the thing directly beneath it. */
.gap-tight    { margin-bottom: var(--s-2); }
/* A field that should not stretch the width of a panel: a short select, a
   code, a name. */
.narrow-field { max-width: 22rem; }
/* A column of panels with no outer frame of its own - the change-password
   page is one panel and does not want the dashboard grid around it. */
.panel-stack  { width: 100%; }

/* Text that needs to catch the eye without being an error banner: a date the
   importer could not read, a locked account. */
.warn-text { color: var(--berry-ink); font-weight: 600; }

/* A panel that is announcing something rather than just holding it. The bar of
   colour along the top is the whole signal. */
.accent-sun   { border-color: var(--sun);   border-top: 4px solid var(--sun); }
.accent-berry { border-color: var(--berry); border-top: 4px solid var(--berry); }

/* A list inside a panel, kept close to the prose above it. */
.tight-list { margin: 0; padding-left: var(--s-5); color: var(--ink-soft); }
.tight-list li + li { margin-top: var(--s-2); }

/* --------------------------------------------------------------------------
   Sign in
   -------------------------------------------------------------------------- */

/* ==========================================================================
   Sign in - frosted glass
   ==========================================================================
   Glass is only glass if there is something behind it worth blurring. A
   translucent panel over a flat colour just looks like a panel with the wrong
   opacity, so the backdrop is built first: four soft brand-coloured lights on
   a deep purple ground, which the card then refracts.

   Everything is self-contained CSS. The Content-Security-Policy allows no
   external images or scripts, and none are needed for this.
   ========================================================================== */

body.signin {
    display: grid;
    place-items: center;
    color: var(--ink);
    background:
        radial-gradient(58rem 58rem at 12% 16%, rgba(248, 115, 46, .55), transparent 62%),
        radial-gradient(50rem 50rem at 88% 10%, rgba(45, 190, 236, .50), transparent 62%),
        radial-gradient(54rem 54rem at 82% 90%, rgba(240, 86, 140, .48), transparent 62%),
        radial-gradient(46rem 46rem at 16% 94%, rgba(255, 192, 46, .42), transparent 62%),
        #2a2140;
    background-attachment: fixed;
    overflow-x: hidden;
}

/* One slow-drifting light, so the glass has something that moves under it.
   Purely decorative: it sits behind everything and is switched off entirely
   for anyone who has asked for less motion. */
body.signin::before {
    content: "";
    position: fixed;
    inset: -30vmax;
    z-index: 0;
    background: radial-gradient(38rem 38rem at 50% 50%, rgba(155, 109, 222, .45), transparent 60%);
    animation: signin-drift 26s ease-in-out infinite alternate;
    pointer-events: none;
}
@keyframes signin-drift {
    from { transform: translate3d(-8%, -6%, 0) scale(1); }
    to   { transform: translate3d(10%,  8%, 0) scale(1.15); }
}
@media (prefers-reduced-motion: reduce) {
    body.signin::before { animation: none; }
}

/* The sign-in page borrows the standard shell, which brings a footer with it.
   On a page that is one centred box, that footer is a stray grey line. */
body.signin .pagefoot { display: none; }
body.signin .wrap { margin: 0; position: relative; z-index: 1; }

.signin-box {
    width: min(440px, 100% - 2.5rem);
    margin: 2rem auto;
    padding: 2.25rem 2rem;
    border-radius: 28px;

    /*
     * The glass itself.
     *
     * saturate() alongside the blur is what stops frosted glass looking grey:
     * it pulls the colour back up in what has just been smeared out, which is
     * the trick Apple's vibrancy materials use.
     *
     * The opacity is .80, and the secondary text on the card is darkened to
     * --glass-muted, because this is a form people sign into every morning and
     * the text has to stay readable where the deep backdrop shows through.
     *
     * Those two numbers are measured, not chosen. At .74 with the normal muted
     * grey the small print came out at 3.30:1 against the darkest part of the
     * background - below the 4.5:1 floor for body text, which is the point
     * where frosted glass stops being a style and starts being a squint.
     */
    background: rgba(255, 255, 255, .80);
    -webkit-backdrop-filter: blur(34px) saturate(180%);
    backdrop-filter: blur(34px) saturate(180%);
    border: 1px solid rgba(255, 255, 255, .55);

    /*
     * Three shadows doing three jobs: the cast shadow that lifts the card off
     * the page, a bright inset top edge which is the specular highlight along
     * the rim of a real pane, and a faint dark inset at the foot so the glass
     * has thickness rather than being a sticker.
     */
    box-shadow:
        0 30px 70px rgba(20, 12, 40, .38),
        inset 0 1px 0 rgba(255, 255, 255, .85),
        inset 0 -1px 0 rgba(58, 47, 85, .06);
}

/*
 * Where the browser cannot blur, translucency is just washed-out text. Go
 * nearly opaque instead and keep the rounded, lifted shape.
 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .signin-box { background: rgba(255, 255, 255, .97); }
}

/* The masthead is one centred block. The logo was centring on its own while
   the title and the school name sat left, which read as a mistake rather than
   a choice. Form labels below stay left, where a form wants them. */
.signin-head { text-align: center; margin-bottom: 1.6rem; }
.signin-logo { display: block; width: 152px; height: auto; margin: 0 auto 1.1rem; }
.signin-box h1 { font-size: 1.4rem; margin: 0 0 .3rem; letter-spacing: -.01em; }
.signin-meta { color: var(--glass-muted); font-size: .82rem; margin: 0; }

/* The fields are glass too, a shade clearer than the card so they read as
   wells cut into it rather than tiles sitting on top. */
.signin-box input[type=email],
.signin-box input[type=password] {
    background: rgba(255, 255, 255, .55);
    border: 1px solid rgba(58, 47, 85, .14);
    box-shadow: inset 0 1px 2px rgba(58, 47, 85, .06);
}
.signin-box input[type=email]:focus,
.signin-box input[type=password]:focus {
    background: rgba(255, 255, 255, .92);
    border-color: var(--grape);
}

.signin-box .btn {
    width: 100%;
    justify-content: center;
    margin-top: .6rem;
    height: 2.9rem;
    border-radius: 999px;
    /* A lit top edge and a shadow in the brand colour, so the button looks like
       it belongs to the same material as the card. */
    background: linear-gradient(180deg, #ff8a4d 0%, var(--coral) 55%, var(--coral-d) 100%);
    border-color: transparent;
    box-shadow:
        0 8px 20px rgba(232, 92, 26, .40),
        inset 0 1px 0 rgba(255, 255, 255, .45);
}
.signin-box .btn:hover {
    background: linear-gradient(180deg, #ff7f3c 0%, var(--coral-d) 60%, #d24f12 100%);
    box-shadow:
        0 10px 26px rgba(232, 92, 26, .48),
        inset 0 1px 0 rgba(255, 255, 255, .45);
}
.signin-box .btn:active { transform: translateY(1px); }

.signin-box .hint { color: var(--glass-muted); }

/* --------------------------------------------------------------------------
   The card editor
   -------------------------------------------------------------------------- */

/*
 * The one bar a teacher keeps in view for half an hour: the child's name, how
 * much is written, and the way out to the print preview. Glass, and blurred a
 * little harder than a panel, because the card content scrolls right under it.
 */
.editor-head {
    display: flex;
    gap: 1.1rem;
    align-items: center;
    flex-wrap: wrap;
    background: rgba(255, 255, 255, .80);
    -webkit-backdrop-filter: blur(24px) saturate(175%);
    backdrop-filter: blur(24px) saturate(175%);
    border: 1px solid rgba(255, 255, 255, .70);
    border-radius: var(--radius-l);
    box-shadow:
        0 10px 30px rgba(58, 47, 85, .12),
        inset 0 1px 0 rgba(255, 255, 255, .90);
    padding: 1rem 1.25rem;
    margin-bottom: 1.25rem;
    position: sticky;
    /* Below the top bar, which now owns top:0. Both at zero and the child's
       name would slide underneath the nav. */
    top: 3.75rem;
    z-index: 20;
}
.editor-who { flex: 1; min-width: 200px; }
.editor-who strong { font-size: 1.15rem; display: block; }
.editor-who span { color: var(--ink-soft); font-size: .87rem; }

/* Section headings inside the editor mirror the printed domains, numbered the
   same way, so a teacher checking their work against the paper is looking at
   the same landmarks. */
.domain { margin-bottom: var(--s-5); }
.domain-head { display: flex; gap: .8rem; align-items: baseline; margin-bottom: .2rem; }
.domain-no {
    font-family: var(--serif);
    font-size: 1.6rem;
    color: var(--berry);
    line-height: 1;
}
.domain-title { font-size: 1.15rem; font-weight: 650; }
.domain-sub { color: var(--ink-soft); font-size: .89rem; margin: 0 0 .9rem; }

/*
 * On a phone the rating table cannot shrink below about 415px - four columns
 * of dots plus a readable skill name - so it used to push the whole page
 * sideways and every panel with it. It scrolls inside its own box instead:
 * the page stays put and only the table moves, which is the one place a
 * sideways drag is expected.
 */
.skills-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.skills { width: 100%; min-width: 30rem; border-collapse: collapse; }
table.skills th {
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-faint);
    font-weight: 700;
    padding: .4rem .3rem;
    text-align: center;
    border-bottom: 1px solid var(--line);
}
table.skills th:first-child { text-align: left; }
table.skills th small { display: block; font-weight: 400; text-transform: none; letter-spacing: 0; font-size: .72rem; opacity: .8; }
table.skills td { padding: .5rem .3rem; border-bottom: 1px solid var(--line-soft); text-align: center; }
table.skills td:first-child { text-align: left; width: 46%; }
/* Share what is left evenly, so the four dots sit under their own headings
   instead of bunching against the right edge. */
table.skills th:not(:first-child), table.skills td:not(:first-child) { width: 13.5%; }
.skill-label { font-weight: 600; font-size: .93rem; }
.skill-desc { color: var(--ink-faint); font-size: .81rem; }

/* The rating control is a radio with the input itself hidden - the label is
   the target, which makes the tap area the whole cell rather than a 13px
   circle. Keyboard focus is drawn on the label via :focus-visible on the
   input, so it stays reachable without a mouse. */
/*
 * position:relative is not decoration. The radio inside is absolutely
 * positioned to hide it, and without a positioned ancestor it escapes to the
 * initial containing block - so once the rating table scrolled sideways, those
 * invisible 1px inputs sat at x=480 and stretched the whole document to match.
 * A page that scrolls sideways on a phone because of an input nobody can see.
 */
.rate { position: relative; display: inline-grid; place-items: center; cursor: pointer; }
.rate input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.rate span {
    width: 22px; height: 22px;
    border-radius: 50%;
    border: 2px solid rgba(58, 47, 85, .16);
    background: rgba(255, 255, 255, .70);
    box-shadow: inset 0 1px 1px rgba(58, 47, 85, .05);
    display: block;
    transition: background .12s ease, border-color .12s ease, transform .12s ease;
}
.rate:hover span { border-color: var(--grape); }
.rate input:checked + span {
    border-color: transparent;
    transform: scale(1.12);
    box-shadow: 0 2px 6px rgba(58, 47, 85, .22), inset 0 1px 0 rgba(255, 255, 255, .40);
}
.rate input:focus-visible + span { outline: 2px solid var(--grape); outline-offset: 3px; }
.rate-1 input:checked + span { background: var(--g1); }
.rate-2 input:checked + span { background: var(--g2); }
.rate-3 input:checked + span { background: var(--g3); }
.rate-4 input:checked + span { background: var(--g4); }

.prompt { margin-top: var(--s-4); }
.prompt label { font-size: .9rem; }

/* Characters remaining, sat under the box it belongs to. Quiet until the room
   is nearly gone - a counter that shouts from the first keystroke is a counter
   people stop seeing. */
.counter {
    display: block;
    text-align: right;
    font-size: .76rem;
    color: var(--ink-faint);
    margin-top: .15rem;
    font-variant-numeric: tabular-nums;
}
.counter-low { color: var(--sun-d); }
.counter-out { color: var(--berry-ink); font-weight: 700; }

.legend { display: flex; gap: var(--s-4); flex-wrap: wrap; margin: 0 0 var(--s-4); padding: 0; list-style: none; }
.legend li { display: flex; gap: .45rem; align-items: center; font-size: .82rem; color: var(--ink-soft); }
.legend i { width: 13px; height: 13px; border-radius: 50%; display: block; }
.legend .l1 { background: var(--g1); }
.legend .l2 { background: var(--g2); }
.legend .l3 { background: var(--g3); }
.legend .l4 { background: var(--g4); }

/* The three "Where I'm Growing Next" rows */
/*
 * Three columns, and each one a cell rather than a bare textarea.
 *
 * The textareas used to be direct children of this grid. Then the character
 * counter arrived, and app.js inserts its little "60 left" span immediately
 * after each box - which made six children per row, not three. The grid dutifully
 * flowed them 1-2-3 then 1-2-3, so every counter landed in the next row's first
 * column and the whole section stepped sideways down the page.
 *
 * A wrapper per column keeps the counter inside its own cell where it belongs.
 */
.growth-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--s-3) var(--s-4); margin-bottom: var(--s-3); align-items: start; }
.growth-row > div { min-width: 0; }
.growth-row textarea { min-height: 4.5rem; }
.growth-head { font-size: .72rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-faint); font-weight: 800; }

/*
 * "182  of  198  days" reads as one sentence and should sit as one. As two
 * fields in the auto-fitting grid they were flung to opposite ends of a
 * 1100px panel with no visible relationship.
 */
.inline-pair { display: flex; align-items: center; gap: .5rem; }
.inline-pair input { width: 6rem; text-align: center; }
.inline-pair span { color: var(--ink-soft); font-size: .92rem; white-space: nowrap; }

/* An attribution line under its quote, not beside it. */
.field.half { max-width: 24rem; }

/* Save bar pinned to the bottom, because these forms are long and a Save
   button at the end of four screens of typing is a Save button nobody finds
   until they have already navigated away. */
.savebar {
    position: sticky;
    bottom: 0;
    display: flex;
    gap: var(--s-3);
    align-items: center;
    flex-wrap: wrap;
    background: rgba(255, 255, 255, .86);
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
    border: 1px solid rgba(255, 255, 255, .70);
    border-radius: var(--radius-l);
    /* Shadow upward, since the page passes underneath it, and tinted with the
       ink rather than the old brown. */
    box-shadow:
        0 -8px 26px rgba(58, 47, 85, .12),
        inset 0 1px 0 rgba(255, 255, 255, .90);
    padding: var(--s-3) var(--s-4);
    margin-top: var(--s-5);
    z-index: 20;
}
.savebar .spacer { flex: 1; }
.savebar .saved-note { color: var(--ink-faint); font-size: .84rem; }

.photo-slot { display: flex; gap: 1.25rem; align-items: flex-start; }
.photo-slot > div:last-child { flex: 1; min-width: 0; }
/* A file input will not shrink below the width of its own button and label,
   so on a narrow screen it pokes out of the panel. Cap it, and let the slot
   wrap so the control gets the full width instead of a squeezed column. */
.photo-slot input[type=file] { max-width: min(22rem, 100%); }
.photo-slot img { width: 76px; height: 92px; object-fit: cover; border-radius: 8px; border: 1px solid var(--line); background: var(--grape-pale); }
.photo-none { width: 76px; height: 92px; border-radius: 8px; border: 1px dashed var(--line); display: grid; place-items: center; color: var(--ink-faint); font-size: .7rem; text-align: center; padding: .3rem; }

/* The three card pictures. One column each, picture on top, the way the strip
   prints, so the panel reads as what it becomes. The preview is the frame's
   own shape and crops from the same point, so what a teacher sees here is
   close to what the card will keep of an upright photograph. */
.trio-edit { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.1rem; }
.trio-slot { display: flex; flex-direction: column; gap: .45rem; min-width: 0; }
.trio-slot > img,
.trio-slot > .photo-none { width: 100%; height: auto; aspect-ratio: 5 / 4; border-radius: 10px; }
.trio-slot > img { object-fit: cover; object-position: center 30%; border: 1px solid var(--line); background: var(--grape-pale); }
.trio-slot input[type=file] { max-width: 100%; }
/* What app.js says after shrinking a photograph in the browser. */
.shrink-note { display: block; font-size: .78rem; color: var(--ink-soft); }
.shrink-note:empty { display: none; }

@media (max-width: 720px) {
    .wrap { width: calc(100% - 1.6rem); margin: 1.1rem auto; }
    .growth-row { grid-template-columns: 1fr; }
    .photo-slot { flex-wrap: wrap; }
    .trio-edit { grid-template-columns: 1fr; }

    /*
     * The data tables cannot shrink below about 460px - four columns of
     * headings and a child's name - so on a phone they used to push the whole
     * page sideways, every panel with it. display:block turns the table into
     * its own scroll box: the page stays put and only the table moves. Rows
     * and cells keep their table layout inside it.
     *
     * This is not a glass problem. It was there before any of this and shows
     * up on the dashboard, the class list, Students, Teachers and Settings.
     */
    table.grid { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
    table.skills td:first-child { width: auto; }
    .whoami { width: 100%; }
    .editor-head { position: static; }
}

/* Anything marked no-print is chrome: it exists to operate the app, not to
   describe a child. view.php is the page meant for paper. */
@media print {
    .topbar, .pagefoot, .savebar, .no-print { display: none !important; }
    body { background: #fff; }
}
