/* ---------------------------------------------------------------------------
   THE ADMINISTRATION APPLICATION'S OWN CHROME.
   
   Everything here was previously unstyled - .screengrid, .screencard,
   .signin-what, .listtable, .plantable, .checkgrid, .setbox all rendered as
   bare HTML, which is why the sign-in and the index looked unfinished while
   the forms looked finished. roomsedit.css styles the FORM controls; this
   styles the pages around them.
   
   IT INVENTS NO COLOURS. Every value comes from app.css's tokens -
   --brand #2f6fed, --surface, --ink, --ink-soft, --line, --line-strong,
   --radius-*, --control-h - so the dark theme works without a single rule
   here mentioning dark, and a change to the product's palette reaches these
   screens too.
   
   ONE ACCENT, SPENT ONCE PER SCREEN. The sign-in card's top edge, the
   pending panel's left edge, and the focused control. Everywhere else is
   surface, line and ink. An administration tool that is busy with colour
   makes the one thing that needs attention harder to see, not easier.
--------------------------------------------------------------------------- */

/* ------------------------------------------------------------------ */
/* The page shell                                                      */
/* ------------------------------------------------------------------ */

/*
 * Mišo, 7 Sep 2026: "footer nam je previse vertikalno daleko od sadržaja
 * stranice - neka bude tanka linija na kraju sadržaja i onda poslije odmah
 * footer".
 *
 * The 72px used to be here so the footer never crowded the last control on
 * the page. It solved that and bought a hole: on a page that ends with a
 * paragraph, seventy-two pixels of nothing reads as "the page failed to
 * finish" rather than as breathing room. A line does the same job in one
 * pixel - it SAYS the content has ended - so the space can go. See .foot in
 * app.css for the line itself.
 */
/* ------------------------------------------------------------------ */
/* THE DENSITY PASS - 13 Sep 2026                                      */
/* ------------------------------------------------------------------ */

/*
 * Mišo, 12 Sep 2026: "mnogo slobodnog prostora vertikalnog je na ovim svim
 * ekranima - malo to kompaktnije moramo pravit u smislu visine! svuda … obicno
 * se zale da je preglednost slabija na webu."
 *
 * MEASURED FIRST (tools/_measure-vertical.py, and the promise was that nothing
 * would move before it was): 973 vertical declarations across the three
 * stylesheets, 67 distinct written values, 47 distinct once resolved to
 * pixels. That reads like chaos and is not - the commonest are 8px (101 uses),
 * 10 (95), 6 (88), 12 (59), 2 (57), 18 (39), 14 (37), and most of the rest sit
 * one or two pixels off one of those. It is DRIFT, not disorder: the same
 * space written as 6, 7 and 8 depending on the day.
 *
 * SO THIS IS NOT A RESCALING. Rewriting forty-seven numbers would move twelve
 * screens slightly and fix nothing anybody complained about. What is actually
 * costing the height is one thing and it is structural:
 *
 *     TWO OWNERS OF THE SAME GAP.
 *
 * This rule is a flex column with `gap`, so it already separates every section
 * of a screen from the next. And then `.setgroup` - the small uppercase
 * heading each section opens with - carried `margin: 22px 0 2px` of its own.
 * Both apply. Every section heading on every screen had 24 + 22 = 46px above
 * it, and neither number was wrong on its own.
 *
 * The chain at the top of a settings tab, in declared pixels, before:
 *
 *     .editwrap padding-top                      20
 *     .tabrow (31 + 16 margin)                   47
 *     gap                                        24
 *     .signin-who (20 + 4)                       24
 *     gap                                        24
 *     h2.setgroup (4 + 14 + 2)                   20
 *     gap                                        24
 *     p.sub                                      17
 *     gap                                        24
 *     thead th (9 + 13 + 9 + 2)                  33
 *                                               ---
 *                                               257px before the first datum
 *
 * After: gap 16 and no margin on a heading the gap already separated - 8px on
 * each of four gaps plus 22, so 54px off the top of that screen and off every
 * screen shaped like it. A fifth of the run-up, for two declarations and no
 * template touched.
 */
.editwrap
{
    max-width: 1180px;
    margin: 0 auto;
    padding: 28px 24px 20px;
    display: flex;
    flex-direction: column;

    /*  WAS 24. It separates one section of a screen from the next, and 24 is
        the right feel for two or three of them; these screens have eight, and
        eight generous gaps is a page nobody can see the bottom of. */
    gap: 16px;
}

/*  THE HEADING GIVES UP ITS OWN MARGIN WHERE THE GAP ALREADY PAID.
 *
 *  SCOPED TO DIRECT CHILDREN, deliberately. Inside a .setform the gap does not
 *  exist - `.setform > * + *` owns that spacing and gives 12px - and inside a
 *  .setcard there is a rule of its own. Removing the margin globally would
 *  make both of those too tight, which is how a density pass turns into a week
 *  of small regressions. */
.editwrap > .setgroup
{
    margin-top: 0;
}

.editwrap-narrow
{
    max-width: 420px;
    padding-top: 56px;
}

.editwrap h1,
.editwrap h2
{
    margin: 0;
    letter-spacing: -0.011em;
}

.editwrap h2
{
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
}

/* ------------------------------------------------------------------ */
/* Sign in                                                             */
/* ------------------------------------------------------------------ */

/*  A CARD, NOT A FORM LOOSE ON THE PAGE. This door is reached by people who
    followed a link somebody sent them - a card says "this is a place you
    arrive at" where a bare form says "you are already inside". */
.editwrap-narrow .form,
.signin-what,
.signin-who
{
    max-width: 420px;
}

.signin-what
{
    font-size: 22px;
    font-weight: 650;
    line-height: 1.25;
    color: var(--ink);
    margin: 0 0 6px;
}

.signin-who
{
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--ink-soft);
    margin: 0 0 4px;

    /*  The sentence that stops a professor typing working credentials into a
        door that will refuse them. It is a note, not a warning: bordered and
        quiet, because it is true for everybody and alarming for nobody. */
    border-left: 2px solid var(--line-strong);
    padding: 2px 0 2px 12px;
}

.editwrap-narrow .form
{
    background: var(--surface);
    border: 1px solid var(--line);
    border-top: 3px solid var(--brand);
    border-radius: var(--radius-l);
    padding: 26px 24px 22px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 12px 28px -22px rgba(0, 0, 0, .45);
}

.field
{
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.field-label
{
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.input
{
    height: var(--control-h);
    padding: 0 11px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-s);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 14.5px;
    width: 100%;
    box-sizing: border-box;
}

.input:focus-visible,
.editwrap input:focus-visible,
.editwrap select:focus-visible,
.editwrap textarea:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 1px;
    border-color: var(--brand);
}

/*
 * A NUMBER IS NOT A SENTENCE - 12 Sep 2026.
 *
 * Mišo: "i inace svi ovi integer unosi su previsoki".
 *
 * Every control on these screens is one height, --control-h, which is the
 * right height for a box somebody types a room name into: comfortable to hit,
 * tall enough for descenders, the same as the buttons beside it. A box that
 * holds "12" inherited it too, and at that width it is nearly SQUARE - which
 * is why a column of them reads as a column of empty boxes rather than as a
 * row of numbers. There are eleven of them on the generation tab alone.
 *
 * Four pixels off each end, and no less: these are still fields somebody
 * clicks with a mouse, and the browser's own spinner arrows live inside them.
 *
 * `-webkit-appearance: textfield` is NOT set - the arrows stay. 1..52 is a
 * range people nudge, and a week number is exactly the kind of value where
 * the arrows are used rather than ignored.
 */
.input[type="number"],
.numbox
{
    height: calc(var(--control-h) - 8px);

    /*  The text sits closer to the edge too, or a narrow box has more padding
        than number in it. */
    padding-inline: 8px;
}

/*  A BUTTON ROW IS NOT THE LAST FIELD - 13 Sep 2026.

    Mišo: "dugmad se previse drzi linije".

    It was `margin-top: 4px` and nothing else, which put Save four pixels under
    whatever ended the form - on the room and course forms, the bottom edge of
    a bordered pick panel. Four pixels reads as "part of that panel" rather
    than as "the thing that ends this screen", and the eye running down the
    form arrives at the button before it has registered that the fields
    stopped.

    18 ABOVE AND 10 BELOW, and both numbers are deliberately modest. The
    vertical-density pass of 12 Sep exists because he had said the opposite
    complaint about these screens - "mnogo slobodnog prostora vertikalnog je na
    ovim svim ekranima" - so this is the smallest separation that reads as a
    break rather than a gap. Roughly the 16px .editwrap already uses between
    its own sections, which is the rhythm the rest of the page is in.

    AND NO BORDER. A rule above the buttons would answer "too close to the
    line" by drawing a second line, which is the shape of fix that makes a
    screen busier while technically doing what was asked.

    EVERY form-actions ON EVERY SCREEN moves, which is the point: this row is
    the same row everywhere, and one of them being roomier than the others is
    how a set of screens stops feeling like one application. */
.form-actions
{
    margin-top: 18px;
    margin-bottom: 10px;
}

/*  A BUTTON IS AS WIDE AS ITS WORD - Mišo, 10 Sep 2026, on Generate:
    "ti start i stop buttons kod generiranja mogu biti normalne sirine - ne na
    cijeli ekran".

    This said width: 100%, which is a PHONE pattern: on a narrow screen a
    full-width button is the easy target and the right one. On a 1600px screen
    it makes Start a bar across the page, which reads as a section rather than
    as something to press - and Stop, drawn the same way, becomes the loudest
    object on a screen whose job is to show progress.

    So: natural width here, full width where it was meant to be, in the phone
    block at the foot of this file. Every form-actions row on every screen is
    affected, which is the point - the Generate screen is only where it was
    noticed first. */
.form-actions .btn
{
    justify-content: center;
}

/* ------------------------------------------------------------------ */
/* The index - what can be edited here                                 */
/* ------------------------------------------------------------------ */

.screengrid
{
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
    gap: 12px;
}

.screencard
{
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 16px 16px 15px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-m);
    text-decoration: none;
    color: inherit;

    /*  Only the border moves on hover. A card that lifts or grows shifts the
        cards beside it, and this grid is read by scanning across it. */
    transition: border-color .12s ease, background-color .12s ease;
}

.screencard:hover,
.screencard:focus-visible
{
    border-color: var(--brand);
    background: color-mix(in srgb, var(--brand) 4%, var(--surface));
}

.screencard:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.screenname
{
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
    letter-spacing: -0.008em;
}

.screennote
{
    font-size: 13px;
    line-height: 1.45;
    color: var(--ink-soft);
}

/*  NOT YET BUILT, and drawn as such rather than hidden. A dashed edge and no
    hover: it reads as a placeholder at a glance, which is the whole point -
    somebody who came for students learns in three seconds that students are
    not here, instead of opening every menu twice. */
.screencard-soon
{
    border-style: dashed;
    background: transparent;
    cursor: default;
}

.screencard-soon .screenname
{
    color: var(--ink-soft);
}

.screencard-soon .screennote
{
    opacity: .8;
}

/* ------------------------------------------------------------------ */
/* What the desktop has not taken yet                                  */
/* ------------------------------------------------------------------ */

/*  THE ONE PANEL ON THE INDEX THAT WANTS ATTENTION. Until a planner's copy
    has drained a change it is live on the published timetable and absent
    from their document, so this gets the accent the screen cards do not. */
.pendingblock
{
    background: color-mix(in srgb, var(--brand) 5%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--brand) 22%, var(--line));
    border-left: 3px solid var(--brand);
    border-radius: var(--radius-m);
    padding: 16px 18px 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.pendingblock .progname
{
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    font-size: 14.5px;
    font-weight: 600;
    color: var(--ink);
}

.editcount
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 20px;
    padding: 0 7px;
    border-radius: 999px;
    background: var(--brand);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.pendingblock .branchnote
{
    margin: 0;
    font-size: 13px;
    color: var(--ink-soft);
}

.pendingblock .usedby
{
    margin: 0;
    padding-left: 1.1em;
    columns: 2;
    column-gap: 28px;
}

.pendingblock .usedby li
{
    font-size: 13px;
    color: var(--ink-soft);
    break-inside: avoid;
}

.branchcounts
{
    color: var(--ink-soft);
    opacity: .75;
    font-variant-numeric: tabular-nums;
}

.adminby
{
    margin-top: 4px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}

/* ------------------------------------------------------------------ */
/* Lists - the course screens, and anything else with rows             */
/* ------------------------------------------------------------------ */

.listtable
{
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

.listtable th
{
    text-align: left;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-soft);
    padding: 0 12px 8px 0;
    border-bottom: 1px solid var(--line-strong);
    white-space: nowrap;
}

.listtable td
{
    padding: 10px 12px 10px 0;
    border-bottom: 1px solid var(--line);
    color: var(--ink-soft);
    vertical-align: top;
}

.listtable tbody tr:hover td
{
    background: color-mix(in srgb, var(--brand) 3%, transparent);
}

.listtable td:first-child
{
    color: var(--ink);
}

.listtable a
{
    color: var(--brand);
    text-decoration: none;
    font-weight: 500;
}

.listtable a:hover
{
    text-decoration: underline;
}

.listtable .num,
th.num, td.num
{
    text-align: right;
    padding-right: 0;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/*  A second line inside a cell - a translation, a branch, a warning about a
    group that sits outside the course's branches. Never competes with the
    first line for the eye. */
.sub
{
    display: block;
    font-size: 12px;
    color: var(--ink-soft);
    opacity: .85;
}

.empty
{
    margin: 0;
    padding: 18px;
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius-m);
    color: var(--ink-soft);
    font-size: 13.5px;
    text-align: center;
}

.sublist
{
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding-top: 22px;
    border-top: 1px solid var(--line);
}

/* ------------------------------------------------------------------ */
/* The plan editor                                                     */
/* ------------------------------------------------------------------ */

/*  THREE COLUMNS OF TEXT, kept narrow on purpose. The hours box takes what a
    planner already writes - "3", "2+1", "1.5" - and a box that is wider than
    its content invites a sentence into a field that parses numbers. */
.plantable
{
    border-collapse: collapse;
    font-size: 14px;
}

.plantable th
{
    text-align: left;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-soft);
    padding: 0 10px 6px 0;
    white-space: nowrap;
}

.plantable td
{
    padding: 0 10px 8px 0;
}

.plantable input
{
    height: var(--control-h);
    padding: 0 9px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-s);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    width: 92px;
}

.plantable td:last-child input
{
    width: 132px;
    font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
}

/* ------------------------------------------------------------------ */
/* Sets of tick boxes - branches, lecturers, groups                    */
/* ------------------------------------------------------------------ */

.setbox
{
    border: 1px solid var(--line);
    border-radius: var(--radius-m);
    background: var(--surface);
    padding: 16px 18px 18px;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.setbox legend
{
    padding: 0 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

/*  COLUMNS, BECAUSE THESE LISTS ARE LONG. A school has 263 lecturers and 681
    groups; one per line is a page nobody scrolls to the bottom of. Capped in
    height with its own scroll so the Save button stays reachable. */
.checkgrid
{
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 2px 18px;
    max-height: 320px;
    overflow-y: auto;
    padding-right: 4px;
}

.check
{
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 5px 4px;
    border-radius: var(--radius-s);
    font-size: 13.5px;
    line-height: 1.4;
    color: var(--ink);
    cursor: pointer;
}

.check:hover
{
    background: color-mix(in srgb, var(--brand) 5%, transparent);
}

.check input
{
    margin: 2px 0 0;
    accent-color: var(--brand);
    flex: none;
}

.check .sub
{
    display: inline;
    margin-left: 5px;
}

/* ------------------------------------------------------------------ */
/* The narrowing form above a list                                     */
/* ------------------------------------------------------------------ */

.pickform
{
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 12px;
    padding: 14px 16px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-m);
}

.pickform label
{
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 1 1 280px;
    min-width: 0;
}

.pickform label > span
{
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.pickform select
{
    height: var(--control-h);
    padding: 0 9px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-s);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    width: 100%;
}

/*  The primary sits at the RIGHT end of this row, away from "Show", because
    "add a course" and "narrow the list" are different intentions and the two
    buttons should not be a pair. */
.pickform .btn-primary
{
    margin-left: auto;
}

/* ------------------------------------------------------------------ */
/* Breadcrumbs in the top bar                                          */
/* ------------------------------------------------------------------ */

/*  The drill-down is four levels deep and the way back up is the only
    navigation on those screens, so the trail is a real control: links look
    like links, the last one does not. */
.weeknav a.weeklabel
{
    color: var(--brand);
    text-decoration: none;
}

.weeknav a.weeklabel:hover
{
    text-decoration: underline;
}

/* ------------------------------------------------------------------ */
/* Small screens                                                       */
/* ------------------------------------------------------------------ */

@media (max-width: 720px)
{
    .editwrap
    {
        padding: 20px 16px 56px;
    }

    /*  See .form-actions .btn above: full width is the phone pattern, and
        this is the phone. */
    .form-actions .btn
    {
        width: 100%;
    }

    .pendingblock .usedby
    {
        columns: 1;
    }

    /*  A table with six columns does not become readable on a phone by
        shrinking; it scrolls in its own box and the page does not. */
    .listtable
    {
        min-width: 620px;
    }

    .sublist,
    .editwrap > .scroll
    {
        overflow-x: auto;
    }
}

/* ---------------------------------------------------------------------------
   THE FRAME - 6 Sep 2026.

   A timetable is a wide, dense picture with a lot of chrome around it, and
   until now all of that chrome was in ONE horizontal bar across the top. That
   bar had grown to the width of the page and was the first thing the eye hit
   on every screen, above a grid that then had to fight for the rest.

   So the chrome is split by WHAT IT IS ABOUT:

     the top bar    this page, and you: the week, help, refresh, language,
                    who you are signed in as. Thin - 52px - because it is
                    read, not worked in.

     the left rail  the application: which screen. Visible, marked, foldable
                    to its icons when the grid needs the width.

     the right rail what this screen still owes you: on the timetable, the
                    lessons not yet placed. Foldable for the same reason.

   The grid sits in the middle with its day names and its hour ladder pinned,
   so scrolling into Thursday afternoon never loses which column is Thursday.

   MEASUREMENTS, AND WHY THESE. 52px of top bar is one 38px control plus its
   breathing room - it cannot go lower without the controls in it touching the
   edges. 210px of rail is the longest label in thirteen languages
   ("Predavatelji", "Studienprogramme") at 13px plus a 16px icon and its gap;
   below that the German wraps, and a navigation list whose items are two
   lines tall stops being scannable. Folded is 52px: the icon, centred, with
   the same 18px of touch either side the top bar's buttons have.

   IT INVENTS NO COLOURS. Same rule as the rest of this file - every value is
   an app.css token, so both schemes work without one rule here saying dark.
--------------------------------------------------------------------------- */

:root
{
    --topbar-h:      52px;
    --lrail-w:      210px;
    --lrail-fold-w:  52px;
}

/*
 * THE MARK ON A FIELD THE SCHOOL MUST ANSWER.
 *
 * Quiet on purpose - it sits beside a label, not in place of one, and the
 * sentence that explains what happens while it is unanswered is a notice at
 * the top of the tab rather than a colour on a box. An asterisk alone has
 * never told anybody anything; it is a reminder for somebody who already
 * read the sentence.
 *
 * aria-hidden at every call site: the requirement is said in words, and a
 * screen reader announcing "star" adds nothing.
 */
.req
{
    color: var(--danger, #b4413a);
    font-weight: 600;
}

/* ------------------------------------------------------------------ */
/* Narrow numbers and icon buttons - SHARED                            */
/* ------------------------------------------------------------------ */

/*  Moved here from availability.css on 7 Sep 2026, when the Settings
    General tab became the second screen to want them. They say nothing
    about any one screen, so they belong in the sheet every screen loads.
    availability.css keeps only what is about availability. */

/* ------------------------------------------------------------------ */
/* Narrow numbers                                                       */
/* ------------------------------------------------------------------ */

/*
 * Mišo, 7 Sep 2026: "ovi brojevi mogu biti uža dugmad i gdje god mozes u nasem
 * gui umjesto tekstovnih buttons koji kradu prostor mozes stavljat ikonice".
 *
 * A row of eight of these is narrower than one dropdown, shows the whole range
 * without opening anything, and picks a value in one click. The rules are
 * written to be borrowed: .numrow, .numbtn, .numbox and .iconbtn say nothing
 * about availability, and the next screen that needs a small number should use
 * them rather than invent a fourth shape.
 */
.numrow
{
    display: flex;
    gap: 2px;
}

.numbtn
{
    min-width: 1.6rem;
    padding: 0.2rem 0;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--surface);
    color: var(--ink-soft);
    font: inherit;
    font-size: 0.8rem;
    line-height: 1.1;
    text-align: center;
    cursor: pointer;
}

.numbtn:hover
{
    border-color: var(--brand);
    color: var(--ink);
}

/*  THE CHOSEN ONE IS FILLED, not merely outlined. This is the only thing on
    the bar that says what the next click will do, so it has to survive being
    glanced at rather than read. */
.numbtn-on,
.numbtn-on:hover
{
    border-color: var(--brand);
    background: var(--brand);
    color: var(--on-brand);
    font-weight: 600;
}

.numbtn:focus-visible,
.numbox:focus-visible,
.iconbtn:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

/*  Three characters wide - 1 to 52 needs two, and the browser's spinner wants
    the third. Narrower than the row of buttons it replaces. */
.numbox
{
    width: 3.4rem;
    padding: 0.25rem 0.35rem;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 0.85rem;
}

/*
 * A BUTTON WHOSE LABEL IS A GLYPH.
 *
 * Square, so a row of them lines up; title and aria-label carry the words, so
 * nothing is lost to somebody who cannot see the glyph or does not recognise
 * it. An icon button with no accessible name is a button nobody can use, and
 * it is the standard way this pattern goes wrong.
 */
.iconbtn
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink-soft);
    font-size: 0.95rem;
    line-height: 1;
    cursor: pointer;
}

.iconbtn:hover
{
    border-color: var(--brand);
    color: var(--brand);
}


/* ------------------------------------------------------------------ */
/* The thin top bar                                                    */
/* ------------------------------------------------------------------ */

/*  Only in the administration application. app.css is a copy of the public
    application's sheet and is overwritten by tools/build-from-web.py on every
    sync, so nothing here may live there - which is also why every rule below
    is qualified by body.admin rather than restyling .topbar outright. */
body.admin .topbar
{
    min-height: var(--topbar-h);
    padding: 6px 16px;
    gap: 12px;

    /*
     * NO FROSTED GLASS IN THE ADMINISTRATION APPLICATION.
     *
     * app.css gives .topbar a 78%-transparent background over
     * `backdrop-filter: blur(14px) saturate(1.4)`. That is a pleasant thing
     * over a student's timetable. Over an administration grid it blurs the
     * top row of a table somebody is reading, and it costs a composited layer
     * on every scroll of a screen that has eighty rooms on it. The public
     * application keeps it; this one has no use for it.
     *
     * HONEST HISTORY, because the reason this rule exists is not the reason
     * it was written. It was added on 7 Sep 2026 as a fix for the "page drawn
     * twice" report, on a diagnosis - a stale sticky backdrop sample - that
     * was WRONG. The real cause was a class-name collision with app.js; see
     * .card-here below. The rule is kept because the two sentences above are
     * true on their own, not because it fixed anything. If the glass is
     * wanted back, deleting these three lines costs nothing.
     *
     * Left in app.css untouched: that file is a copy of the public
     * application's sheet and build-from-web.py overwrites it on every sync.
     */
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

/*  The wordmark drops half a step. It is an anchor, not a control, and at the
    same size as the buttons beside it the two compete. */
body.admin .topbar .brand
{
    font-size: 15.5px;
}

/* ------------------------------------------------------------------ */
/* The shell: rail, page, rail                                         */
/* ------------------------------------------------------------------ */

body.admin .shell
{
    display: flex;
    align-items: flex-start;
}

/*
 * THE SHELL IS AT LEAST A SCREENFUL, AND THAT IS WHAT KEEPS THE FOOTER OFF
 * THE RAIL - 7 Sep 2026.
 *
 * The left rail is `position: sticky` with `max-height: calc(100vh -
 * topbar)`, so on any screen it is close to a full viewport tall. The shell's
 * height is its tallest child - normally the page - but a SHORT page (the
 * "this account may not edit" refusal, an empty list, the settings screen on
 * its first tab) is a few hundred pixels, and then the footer, which follows
 * the shell, began where the page ended and was drawn straight across the
 * bottom of the rail. Mišo, with a screenshot of "Settings" and "wise
 * timetable" printed on top of each other: "footer je preblizu sad gore".
 *
 * It was NOT the thin line that caused it - that is what he asked for and it
 * stays. The previous 72px of empty space above the footer had been hiding
 * this, on those pages, by accident.
 *
 * THE FIRST ATTEMPT WAS `min-height` ON THE SHELL, and it was half right: the
 * footer no longer crossed the rail, but it landed just BELOW the fold, so a
 * short page had to be scrolled to see a footer that had nothing under it.
 * Mišo: "footer nek bude zalijepljen na dnu - da ga se drzi na maloj
 * razdaljini".
 *
 * So the page is a column instead. The body is at least a viewport tall, the
 * shell takes whatever is left over, and the footer sits on the bottom edge
 * with a small gap above it - the ordinary sticky-footer arrangement, which
 * also solves the rail by construction: the shell can never be shorter than
 * the space above the footer, so the rail has room and nothing overlaps.
 */
body.admin
{
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

body.admin .shell
{
    /*  GROWS, NEVER SHRINKS. `1 0 auto` and not `1 1 auto`: a week wide and
        tall enough to scroll must push the footer down the page, not be
        squeezed to fit above it. */
    flex: 1 0 auto;
}

/*  A SMALL DISTANCE, AND NOT MORE. Mišo: "footer nek bude zalijepljen na dnu -
    da ga se drzi na maloj razdaljini". The thin line stays flush with the end
    of the content on a page that fills the screen; this is the gap on a page
    that does not, so the footer is not welded to the last row of a short
    list. */
body.admin .foot
{
    margin-top: 14px;
}

body.admin .shell > main
{
    flex: 1 1 auto;

    /*  WITHOUT THIS THE GRID CANNOT SHRINK. A flex item's default minimum is
        the width of its content, so a week wide enough to scroll pushes the
        whole shell sideways instead of scrolling inside itself - and takes
        the rail off the left of the screen with it. */
    min-width: 0;
}

/*
 * THE RAIL'S DIVIDER IS THE COLUMN'S EDGE, NOT THE LIST'S UNDERLINE
 * - 12 Sep 2026.
 *
 * Mišo, with a second screenshot of the bottom of the page: "neki clash sa
 * linijama je - druga slika, dole na dnu".
 *
 * The line was `border-right` on .lrail, and .lrail is `align-self:
 * flex-start` - so it is exactly as tall as its own list of entries. On any
 * page taller than the list, and on every page at all since the shell became
 * a sticky-footer column (it now always reaches the footer), the line STOPPED
 * IN MID-AIR: a 1px vertical stroke ending a few dozen pixels above the
 * footer's horizontal rule, immediately over our own mark in the footer's
 * corner. Two lines that nearly meet read as a mistake, and they were one.
 *
 * SO THE LINE IS DRAWN ON THE SHELL INSTEAD, as a one-pixel background stripe
 * at the rail's width. The shell is the full-height box - that is the whole
 * point of the sticky-footer column above - so the stripe runs from the top
 * bar to the footer's rule and the two meet in a corner.
 *
 * WHY NOT A WRAPPER ELEMENT. The usual fix is an outer column that stretches
 * and an inner box that sticks, and it would mean a new div in _leftrail.php
 * - which several selectors reach through (`.lrail-folded ~ main`), every one
 * of which would silently stop matching. A background stripe changes no
 * structure at all.
 *
 * THE FOLDED RAIL IS NARROWER, and `:has()` is how the shell learns that: the
 * fold class is on the rail, which is the shell's child. Where `:has()` is
 * not supported the rail keeps its own border and this rule never applies -
 * which is the old drawing, not a broken one.
 */
@supports selector(:has(*))
{
    /*  WHERE THE COLUMN'S EDGE IS, ON THE BODY - because TWO elements need
        the number and they are siblings, so neither can inherit it from the
        other. The rail's fold class is deep inside the body, which is exactly
        what `:has()` is for. */
    body.admin
    {
        --lrail-edge: var(--lrail-w);
    }

    body.admin:has(.lrail-folded)
    {
        --lrail-edge: var(--lrail-fold-w);
    }

    /*  ONLY A SHELL THAT HAS A RAIL. A screen that draws the shell without
        one - and there is no reason a later screen could not - would
        otherwise get a vertical line at 210px with nothing on either side of
        it, which is worse than the bug this fixes. */
    body.admin .shell:has(> .lrail)
    {
        background-image: linear-gradient(
            to right,
            transparent calc(var(--lrail-edge) - 1px),
            var(--line)  calc(var(--lrail-edge) - 1px),
            var(--line)  var(--lrail-edge),
            transparent  var(--lrail-edge)
        );
        background-repeat: no-repeat;
    }

    body.admin .lrail
    {
        border-right: 0;
    }

    /*
     * AND THE FOOTER'S RULE STARTS WHERE THE RAIL ENDS - "ova linija isto
     * presjeca sve dole", 12 Sep 2026.
     *
     * The footer's `border-top` ran the whole width of the window, straight
     * across the bottom of the rail column and through our own mark sitting
     * in it. A horizontal line cutting a vertical one it does not belong to
     * is the same fault as the dangling line above, seen from the other side.
     *
     * So the rule is a background stripe too, starting at the rail's edge.
     * The two lines now meet in a corner: the rail's column runs from the top
     * bar down to the bottom of the page, and the footer's rule closes the
     * page beside it.
     *
     * `body.admin .foot` only - the public timetable has no rail and keeps
     * its own border, which this never touches.
     */
    body.admin .shell:has(> .lrail) + .foot
    {
        border-top: 0;

        background-image: linear-gradient(
            to bottom,
            var(--line) 0,
            var(--line) 1px,
            transparent 1px
        );
        background-repeat: no-repeat;
        background-position: var(--lrail-edge) 0;
        background-size: calc(100% - var(--lrail-edge)) 1px;
    }

    @media (max-width: 900px)
    {
        body.admin .shell:has(> .lrail) + .foot
        {
            border-top: 1px solid var(--line);
            background-image: none;
        }
    }

    /*  AND NOT AT ALL WHEN THE RAIL IS A ROW. Under 900px the rail sits
        ACROSS the top of the page rather than down its side (see "Narrow
        screens" below), so a vertical stripe at 210px would be a line through
        the middle of the content with nothing on either side of it. */
    @media (max-width: 900px)
    {
        body.admin .shell
        {
            background-image: none;
        }
    }
}

/* ------------------------------------------------------------------ */
/* The left rail                                                       */
/* ------------------------------------------------------------------ */

.lrail
{
    flex: 0 0 var(--lrail-w);
    width: var(--lrail-w);

    /*  STICKY, NOT FIXED. Fixed takes it out of the flow and then the page
        has to reserve its width by hand, in every layout, forever. Sticky
        keeps it a column that happens not to scroll away. */
    position: sticky;
    top: var(--topbar-h);
    align-self: flex-start;

    /*  A rail taller than the window scrolls on its own rather than pushing
        the page down - twelve screens will not fit a laptop. THE SCROLLING IS
        ON THE INNER BOX, not here: the fold button overhangs this element's
        right edge, and a scroll container clips both axes, so putting
        overflow on the rail itself cut the chevron in half. */
    /*
     * AND THE FOOTER'S HEIGHT COMES OFF IT - 12 Sep 2026. Mišo, with a
     * screenshot of "Settings" and our own mark drawn one on top of the
     * other: "jedino nesto se sudara sa logom dole".
     *
     * This was `calc(100vh - var(--topbar-h))`, which is the space between
     * the top bar and the bottom of the WINDOW - and the footer lives in the
     * last hundred pixels of that space. Measured at 1180x600 with the rail
     * full: the shell ended at 492, the footer began at 506, and the rail's
     * box ran to 544 - fifty-two pixels INTO the footer, landing exactly on
     * the wise timetable mark in the corner.
     *
     * Why it escapes at all: a sticky box is shifted down by `top` once the
     * page scrolls, and when its height already equals its container's there
     * is nowhere for that shift to come from, so it hangs out of the bottom.
     * The cure is to make it shorter by what is underneath it.
     *
     * `--foot-h` IS A RESERVATION, NOT A MEASUREMENT - CSS cannot ask a
     * sibling how tall it is. 7rem is the footer at its usual height (95px)
     * plus its 14px of margin and a little room; below 900px, where the
     * badges wrap and it grows, the rail is a ROW across the top and none of
     * this applies. The cost of the reservation is at most 7rem of unused
     * height at the bottom of a rail that is already scrolling inside itself,
     * which nobody can see.
     */
    max-height: calc(100vh - var(--topbar-h) - var(--foot-h, 7rem));

    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 14px 10px 16px;
    border-right: 1px solid var(--line);
}

.lrail-scroll
{
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-height: 0;
    overflow-y: auto;
}

.lrail-folded
{
    flex-basis: var(--lrail-fold-w);
    width: var(--lrail-fold-w);
    padding-left: 6px;
    padding-right: 6px;
    align-items: center;
}

/*  The centring is on the inner box too: the rail's own children are now the
    fold button and the scroller, not the items. */
.lrail-folded .lrail-scroll
{
    align-items: center;
}

.lrail-head
{
    margin: 14px 0 4px 10px;
    font-size: 10.5px;
    font-weight: 650;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.lrail-head:first-child
{
    margin-top: 2px;
}

/*  FOLDED, THE HEADINGS BECOME A LINE. The words do not fit and hiding them
    outright would run two unrelated groups together into one list of eight.
    A hairline says "these are separate" in 52px. */
.lrail-folded .lrail-head
{
    width: 20px;
    height: 1px;
    margin: 10px 0 6px;
    overflow: hidden;
    text-indent: -999px;
    background: var(--line-strong);
}

.lrail-item,
.lrail-fold
{
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 10px;
    min-height: 34px;
    border-radius: var(--radius-s);
    color: var(--ink);
    text-decoration: none;
    font-size: 13px;
    line-height: 1.2;
    white-space: nowrap;
}

.lrail-item:hover
{
    background: var(--surface-2);
}

.lrail-icon
{
    flex: 0 0 16px;
    color: var(--ink-soft);
}

.lrail-item:hover .lrail-icon
{
    color: var(--ink);
}

/*  WHERE YOU ARE. A left edge in the brand colour and a slightly heavier
    label - not a filled blue pill. The rail is beside the thing being read
    all day, and a saturated block in the corner of the eye is a thing the eye
    keeps going back to for no reason. */
.lrail-here
{
    position: relative;
    font-weight: 600;
    background: var(--surface-2);
    cursor: default;
}

.lrail-here::before
{
    content: "";
    position: absolute;
    left: -10px;
    top: 6px;
    bottom: 6px;
    width: 3px;
    border-radius: 0 2px 2px 0;
    background: var(--brand);
}

.lrail-here .lrail-icon
{
    color: var(--brand);
}

.lrail-folded .lrail-item,
.lrail-folded .lrail-fold
{
    justify-content: center;
    padding: 0;
    width: 40px;
}

.lrail-folded .lrail-here::before
{
    left: -6px;
}

.lrail-folded .lrail-label
{
    /*  NOT display:none. A screen reader should still read the rail as a list
        of named places when it is folded - the labels are hidden from the
        eye, not removed from the page. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.lrail-fold
{
    margin-top: auto;
    padding-top: 12px;
    color: var(--ink-soft);
    font-size: 12px;
}

.lrail-fold:hover
{
    color: var(--ink);
}

/* ------------------------------------------------------------------ */
/* The grid, with its edges pinned                                     */
/* ------------------------------------------------------------------ */

body.admin .gridwrap
{
    padding-left: 18px;
    padding-right: 18px;
}

/*  THE DAY NAMES STAY. Scrolling into Thursday afternoon and losing which
    column is Thursday is the single most common way to misread a timetable,
    and it is the reason this whole wave exists.

    top is the top bar's height because the top bar is sticky too: without it
    the day names slide underneath the bar and are read through the blur. */
body.admin .grid-head
{
    position: sticky;
    top: var(--topbar-h);
    z-index: 6;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}

/*  AND THE HOUR LADDER STAYS, sideways. A week too wide for the window
    scrolls horizontally, and an hour ladder that scrolls away with it leaves
    a field of cards with no vertical scale at all. */
body.admin .grid-hours
{
    position: sticky;
    left: 0;
    z-index: 4;
    background: var(--surface);
}

/*  The corner is in both directions at once, so it needs to outrank both. */
body.admin .grid-corner
{
    position: sticky;
    left: 0;
    z-index: 7;
    background: var(--surface);
}

/* ------------------------------------------------------------------ */
/* The right rail: what this screen still owes you                     */
/* ------------------------------------------------------------------ */

/*  Same treatment as the left one and for the same reason: it is a column
    beside the grid, it should not scroll away, and when the grid needs the
    width it should fold to a strip that still says how many are waiting. */
body.admin .gridsplit > .todo
{
    position: sticky;
    top: calc(var(--topbar-h) + 8px);
    align-self: flex-start;
    max-height: calc(100vh - var(--topbar-h) - 24px);
    overflow-y: auto;
}

.rrail-fold
{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    min-height: 26px;
    margin-left: auto;
    border-radius: var(--radius-s);
    color: var(--ink-soft);
    text-decoration: none;
}

.rrail-fold:hover
{
    background: var(--surface-2);
    color: var(--ink);
}

/*  FOLDED, IT IS A STRIP WITH A NUMBER ON IT. Not nothing: "17 not placed"
    is the whole reason the panel is there, and a fold that hides the count
    hides the alarm along with the detail. */
body.admin .gridsplit > .todo-folded
{
    flex: 0 0 44px;
    width: 44px;
    min-width: 44px;
    padding: 10px 6px;
    overflow: hidden;
}

.todo-folded .todo-list,
.todo-folded .todo-hint,
.todo-folded .todo-note,
.todo-folded .todo-none,
.todo-folded .todo-head-text
{
    display: none;
}

.todo-folded .todo-head
{
    flex-direction: column;
    gap: 8px;
    align-items: center;
}

.todo-folded .rrail-fold
{
    margin: 0;
}

/* ------------------------------------------------------------------ */
/* Narrow screens                                                      */
/* ------------------------------------------------------------------ */

/*  UNDER 900px THE RAIL IS A ROW. There is not enough width for a column of
    labels beside a grid on a laptop in a split window, let alone a tablet,
    and a rail that eats a quarter of a 800px screen is a rail that has to be
    folded before the application can be used - which is a design that has
    given the reader a chore.

    A horizontal strip of the same five items costs 40px of height and no
    width at all, and it keeps the one property that made the rail worth
    building: the screens are all visible, in the same order, all the time. */
@media (max-width: 900px)
{
    body.admin .shell
    {
        display: block;
    }

    .lrail,
    .lrail-folded
    {
        width: auto;
        max-height: none;
        padding: 6px 12px;
        border-right: 0;
        border-bottom: 1px solid var(--line);
        top: var(--topbar-h);
    }

    .lrail-scroll,
    .lrail-folded .lrail-scroll
    {
        flex-direction: row;
        align-items: center;
        gap: 2px;
        overflow-x: auto;
        overflow-y: visible;
    }

    /*  No fold on a narrow screen: the rail is a strip of five items across
        the top, and there is no width for it to give back. */
    .lrail-fold
    {
        display: none;
    }

    .lrail-head,
    .lrail-fold
    {
        display: none;
    }

    .lrail .lrail-label
    {
        position: static;
        width: auto;
        height: auto;
        margin: 0;
        clip-path: none;
    }

    .lrail-here::before
    {
        left: 6px;
        right: 6px;
        top: auto;
        bottom: -6px;
        width: auto;
        height: 3px;
        border-radius: 2px 2px 0 0;
    }

    body.admin .gridsplit > .todo
    {
        position: static;
        max-height: none;
    }
}

/*  The right rail's heading is now a row: the words, the count, and the fold
    pushed to the far edge. It was an <h2> with two things loose in it. */
body.admin .todo-head
{
    display: flex;
    align-items: center;
    gap: 8px;
}

/*  Folded, the count is the only thing left, so it stops being a footnote
    beside a title and becomes the title. */
.todo-folded .editcount
{
    font-size: 13px;
    font-weight: 650;
}

/* ------------------------------------------------------------------ */
/* A lesson card, in the dark                                          */
/* ------------------------------------------------------------------ */

/*  THE CARDS WERE UNREADABLE IN DARK MODE, and nobody had looked.

    schedule.css mixes the execution type's colour into WHITE:

        background: color-mix(in srgb, var(--card) 14%, #fff);

    which is a pale tint in both schemes - while the text on it inherits
    --ink, and --ink in the dark scheme is #e8eaf0. Near-white lettering on
    pale orange. Every lesson on the grid, on every dark machine.

    THE FIX IS THE SURFACE TOKEN, not a colour. --surface is #ffffff in the
    light scheme, so the light grid is byte-identical to what it was; in the
    dark scheme it is #181c23, so the same 14% mix lands on a dark card and
    the white lettering is right after all. The type's colour still carries
    the meaning, on the border and in the tint.

    26% rather than 14% in the dark: the same proportion that reads as a clear
    tint against white is nearly invisible against #181c23.

    THE PUBLIC APPLICATION HAS THE SAME BUG. This file cannot fix it - app.css
    and schedule.css are copies, overwritten by tools/build-from-web.py - and
    it is a change to what every student sees, so it is Mišo's call and not a
    side effect of an administration patch. */
body.admin .card
{
    background: color-mix(in srgb, var(--card) 14%, var(--surface));
}

@media (prefers-color-scheme: dark)
{
    body.admin .card
    {
        background: color-mix(in srgb, var(--card) 26%, var(--surface));
    }
}

/*  The secondary lines carried a hard-coded grey with --muted in front of it,
    and --muted is not one of app.css's tokens - so every card used #6b7280,
    which is below the contrast floor on both cards. --ink-soft is the token
    that exists and it is defined in both schemes. */
body.admin .card-type,
body.admin .card-who,
body.admin .card-room
{
    color: var(--ink-soft);
}

/* ------------------------------------------------------------------ */
/* A card that will TRADE PLACES with the one being moved               */
/* ------------------------------------------------------------------ */

/*  Mišo, 8 Sep 2026: "oznacilo sve pozicije gdje bi se moglo pomjerit i sve
    kartice sa kojim bi se moglo zamijenit mjesto".

    IT IS A <button>, NOT AN <a>, because pressing it WRITES - so it has to
    inherit everything .card sets and then undo what a browser does to a button:
    the user-agent border, background, font and centred text would otherwise
    make one card on the grid look nothing like its neighbours.

    A DASHED OUTLINE AND NOT A FILL. The card keeps its own subject colour -
    the planner is choosing BETWEEN partners and needs to read them - so what
    marks it is a border, in the same dashed idiom the free-hour blocks already
    use for "you may click here". One vocabulary for the whole gesture: dashed
    means available, and the green rectangles and these cards are two answers
    to one question. */
body.admin button.card
{
    appearance: none;
    border: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

body.admin .card-swap
{
    outline: 2px dashed var(--brand);
    outline-offset: -2px;
    box-shadow: 0 0 0 1px var(--surface) inset;
}

body.admin .card-swap:hover,
body.admin .card-swap:focus-visible
{
    outline-style: solid;
    background: color-mix(in srgb, var(--brand) 16%, var(--surface));
}

@media (prefers-color-scheme: dark)
{
    body.admin .card-swap:hover,
    body.admin .card-swap:focus-visible
    {
        background: color-mix(in srgb, var(--brand) 30%, var(--surface));
    }
}

/*  The count in the moving note, so the sentence that says how many partners
    there are reads as part of the same offer the dashed cards make. */
body.admin .placenote-swaps
{
    font-weight: 600;
}

/* ------------------------------------------------------------------ */
/* Counting columns                                                    */
/* ------------------------------------------------------------------ */

/*  "EXECUTION TYPESTURNSLESSONS" - the three counting columns on the course
    list ran into one word.

    The table is width:100% with automatic layout, so the browser gives the
    space to the widest content, which is a course name; the three headings
    are then squeezed narrower than their own text, and right-aligned text in
    a cell narrower than itself overflows LEFT, straight over the heading
    before it. Nothing was wrong with the padding - there was no column left
    to pad.

    A minimum width is what auto layout actually respects, and wrapping lets a
    two-word heading use two lines instead of demanding one long one. 74px is
    "Execution" at .8rem, which is the longest single word any of these
    headings has in the thirteen languages. */
body.admin .edittable th.num,
body.admin .listtable th.num
{
    width: 88px;
    white-space: normal;
    vertical-align: bottom;
}

/* ------------------------------------------------------------------ */
/* The pick set                                                        */
/* ------------------------------------------------------------------ */

/*  A long list of checkboxes with a search box over it - see
    templates/_pickset.php for what it replaced and why it is still
    checkboxes underneath.

    NOTHING HERE APPLIES UNTIL admin.js SAYS is-live. The search box is not
    drawn and the list is not capped, so a browser with no script gets the
    plain, complete list it got before this widget existed. A search box that
    does not search is worse than no search box, and a list capped at 260px
    with no way to filter it is a list with things hidden behind a scrollbar
    for no reason. */

.pickset-bar
{
    display: none;
}

.pickset.is-live .pickset-bar
{
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}

.pickset-search
{
    flex: 1 1 auto;
    min-width: 0;
    min-height: var(--control-h);
    padding: 0 12px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-s);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 13px;
}

.pickset-search:focus
{
    outline: 2px solid var(--brand);
    outline-offset: -1px;
    border-color: var(--brand);
}

/*  HOW MANY ARE TICKED, OUT OF HOW MANY THERE ARE. The second number is why
    the widget exists and the first is the answer to the question the reader
    actually has - "have I already chosen something here?" - which a wall of
    checkboxes could only answer by being read from top to bottom. */
.pickset-count
{
    flex: 0 0 auto;
    color: var(--ink-soft);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.pickset.is-live .pickset-list
{
    /*  Deep enough to show that it is a list and that it scrolls - about
        seven rows - and shallow enough that Save is still on the screen. That
        was the whole complaint: four hundred checkboxes between the reader
        and the button. */
    max-height: 244px;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--line);
    border-radius: var(--radius-s);
    background: var(--surface);
}

.pickset-row
{
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 6px;
    border-radius: var(--radius-s);
    font-size: 13px;
    cursor: pointer;
}

/*  AND display:flex ABOVE BEATS [hidden].

    The script hides a row that does not match by setting el.hidden, which
    works through the user-agent rule `[hidden] { display: none }` - and that
    rule is a plain attribute selector, so ANY class rule setting display wins
    over it. The first version of this filter appeared to do nothing at all:
    the property was being set on every row and every row stayed on the
    screen. Nothing in the console, nothing in the markup to see. */
.pickset-row[hidden]
{
    display: none;
}

.pickset-row:hover
{
    background: var(--surface-2);
}

.pickset-row input
{
    flex: 0 0 auto;
    margin: 0;
    accent-color: var(--brand);
}

.pickset-text
{
    min-width: 0;
}

/*  A TICKED ROW IS MARKED, not just ticked. The chosen ones are first in the
    list and stay visible through a search, so they need to read as a group
    rather than as the top of one - otherwise the first unticked row below
    them looks chosen too. */
.pickset-row:has(input:checked)
{
    background: color-mix(in srgb, var(--brand) 8%, transparent);
    font-weight: 550;
}

.pickset-row:has(input:checked) + .pickset-row:not(:has(input:checked))
{
    margin-top: 6px;
    padding-top: 8px;
    border-top: 1px solid var(--line);
    border-radius: 0 0 var(--radius-s) var(--radius-s);
}

/*  display:inline, because `.sub` on its own is display:block a thousand lines
    above - it is the second line of a card elsewhere in this file. Here it is
    the quiet half of one row, and a row that is two lines tall halves how many
    of them fit in the list this widget exists to shorten. */
.pickset-text .sub
{
    display: inline;
    margin-left: 6px;
    color: var(--ink-soft);
    font-weight: 400;
    font-size: 12px;
}

.pickset-none
{
    margin: 8px 2px 0;
    color: var(--ink-soft);
    font-size: 12px;
}

/* ------------------------------------------------------------------ */
/* The fold, on the rail's edge                                        */
/* ------------------------------------------------------------------ */

/*  IT WAS AT THE BOTTOM OF THE LIST AND NOBODY FOUND IT. The rail was in
    front of Mišo for a day: "usput (nisam vidio) skrivanje raila". Now it is
    a chevron sitting ON the rail's outer border, at the top - the one shape
    every application that folds a rail already uses, so nobody has to be
    taught it. */
.lrail
{
    position: sticky;   /* restated: the button below is positioned to it */
}

.lrail-fold
{
    position: absolute;
    top: 10px;
    right: -11px;
    z-index: 2;

    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;

    border: 1px solid var(--line-strong);
    border-radius: 50%;
    background: var(--surface);
    color: var(--ink-soft);
    box-shadow: var(--shadow-1);
}

.lrail-fold:hover
{
    border-color: var(--brand);
    color: var(--brand);
}

/*  The list starts below it rather than beside it: a heading that runs under
    a floating button is a heading with a hole in it. */
.lrail-head:first-of-type
{
    margin-top: 20px;
}

.lrail-folded .lrail-fold
{
    right: -11px;
    width: 22px;
}

/*
 * DENSER, SO THE RAIL FITS WITHOUT A SCROLL BAR - 11 Oct 2026.
 *
 * Mišo, with a screenshot of the rail scrolling on his laptop (about 920 CSS
 * pixels of window): "vertikalni scroll bar na left rail jos nije potreban i
 * kad bi dodali jos par items". With the two student modules the rail is
 * eighteen places under four headings, and it had become twenty pixels
 * taller than the room left between the top bar and the footer's
 * reservation (--foot-h above). Exam periods and exam registration will add
 * two more.
 *
 * The room is taken out of the rows, not out of the reservation: the
 * reservation is what keeps the rail off the footer's mark, and that bug
 * has been fixed once already. A row of 29px is still a comfortable mouse
 * target at 13px type; the headings keep their air above, a little less of
 * it. Measured in Chromium at 1600 x 922 on the placements screen: the
 * eighteen places took 772px against 728px of room, so it scrolled; now they
 * take 658px, and twenty (with the two exam modules) take 718px of 738px.
 *
 * ONLY ABOVE 900px. Below that the rail is a row across the top (the media
 * query above), with its own padding, and these rules must not reach it.
 *
 * Where a window is genuinely too short, the inner box still scrolls - with
 * a thin bar rather than a full-width one, so it does not look like a
 * second page beside the page.
 */
@media (min-width: 901px)
{
    .lrail
    {
        padding-top: 10px;
        padding-bottom: 10px;
    }

    .lrail-item
    {
        min-height: 29px;
    }

    .lrail-head
    {
        margin-top: 8px;
        margin-bottom: 3px;
    }

    /*  The first heading keeps clear of the fold chevron that sits on the
        rail's edge beside it (.lrail-fold, top: 10px, 22px tall). */
    .lrail-head:first-of-type
    {
        margin-top: 18px;
    }

    .lrail-scroll
    {
        scrollbar-width: thin;
    }
}

/* ------------------------------------------------------------------ */
/* The drawer: a list you ask for                                      */
/* ------------------------------------------------------------------ */

/*  WHAT IS NOT PLACED USED TO BE A RAIL, and it is a drawer now.
    Mišo, 6 Sep: "nisam siguran da je dobra ideja desnom railu dat
    neraspoređene - tih može bit hiljade - i neka se pojave u dedicated
    prostoru kad treba on demand."

    A rail is for something you glance at all day. Two thousand unplaced
    lessons are not glanceable at any width - they are a job you open, work
    through and close. The COUNT stays on the toolbar button, so the alarm is
    on the screen even when the list is not.

    FIXED, NOT A COLUMN. It covers part of the grid rather than taking width
    from it, which is what Mišo described for the settings panel too - "iz
    menija na lijevom railu izvučeš pravougaonik koji prekrije dio grida". The
    grid underneath keeps every pixel it had. */
.drawer
{
    position: fixed;
    top: var(--topbar-h);
    right: 0;
    bottom: 0;
    z-index: 40;

    display: flex;
    flex-direction: column;
    width: min(420px, 94vw);
    padding: 14px 16px 18px;

    border-left: 1px solid var(--line-strong);
    background: var(--surface);
    box-shadow: var(--shadow-3);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/*  THE ONE DRAWER THAT COMES FROM THE LEFT.
    
    The other three on the schedule screen are about a LESSON - the one you
    clicked, over on the right where you clicked it. This one is about the grid
    as a whole and it is pulled out of the left rail, so it arrives from there
    and covers the part of the grid nearest it. Mišo described it that way in
    the first place: "iz menija na lijevom railu izvuces pravougaonik koji
    prekrije dio grida".
    
    It starts where the rail ends. The rail has two widths and the fold puts a
    class on the rail itself, which is a SIBLING of the <main> this drawer
    lives in - so the second rule catches the folded case without any script
    and without the body having to carry the state. */
.drawer-set
{
    left: var(--lrail-w);
    right: auto;
    border-left: 0;
    border-right: 1px solid var(--line-strong);
}

.lrail-folded ~ main .drawer-set
{
    left: var(--lrail-fold-w);
}

/*  On a phone the rail is not a column beside the grid, so neither is this. */
@media (max-width: 720px)
{
    .drawer-set
    {
        left: 0;
    }
}

.drawer-head
{
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
}

.drawer-title
{
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    font-size: 15px;
    font-weight: 650;
}

.drawer-shut
{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    margin-left: auto;
    border-radius: var(--radius-s);
    color: var(--ink-soft);
    text-decoration: none;
}

.drawer-shut:hover
{
    background: var(--surface-2);
    color: var(--ink);
}

/* ==================================================================== */
/*  The approval letters - School settings, Letters - 14 Sep 2026        */
/* ==================================================================== */

/*  THE PLACEHOLDER LIST. A definition list rather than a table: it is four
    short pairs and a table would draw borders round something that is a
    glossary, not data. Two columns on anything but a phone, because the mark
    and its meaning belong on one line. */
.ltmarks
{
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 14px;
    margin: 0 0 22px;
    max-width: 640px;
}

.ltmarks dt
{
    margin: 0;
}

.ltmarks dd
{
    margin: 0;
    color: var(--ink-soft);
}

.ltmarks code
{
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
}

/*  Each letter is its own form, so the two need space between them or the
    second heading reads as a footnote to the first Save. */
.ltrole
{
    margin-bottom: 28px;
    max-width: 720px;
}

.ltfield
{
    max-width: 720px;
}

/*  THE LETTER ITSELF, IN A MONOSPACE BOX. It goes out as plain text - the
    lecturer sees the line breaks that were typed - so the box has to show the
    same shape the reader will get. A proportional font would make a carefully
    laid-out signature look ragged here and fine in the inbox, or the reverse.

    `resize: vertical` because somebody writing a long letter wants a taller
    box, and nobody wants a wider one: the width is the width of the page. */
.ltbody
{
    width: 100%;
    font-family: var(--mono, ui-monospace, "Cascadia Mono", "Consolas", monospace);
    font-size: 13px;
    line-height: 1.5;
    resize: vertical;
}

/* ==================================================================== */
/*  The pause mapping - School settings, Hours - 13 Sep 2026             */
/* ==================================================================== */

/*  TWENTY TO SIXTY ROWS OF THREE SHORT NUMBERS. Left to the ordinary table
    rules they would stretch the full width of the screen with a 12-character
    box floating in an 800px cell, which reads as a form somebody forgot to
    finish. Capped, and the columns sized to what is in them. */
.hrstable
{
    max-width: 420px;
}

.hrstable td,
.hrstable th
{
    /*  Tighter than a list of rooms: these rows are one number each and the
        eye is running down a column, not reading across. */
    padding-block: 4px;
}

/*  The key, and under it what the key MEANS - "7" over "3:30". The number is
    the thing being indexed, so it leads; the reading is the smaller line. */
.hrskey
{
    white-space: nowrap;
}

.hrsbox
{
    width: 72px;
    text-align: right;
}

/*  The default sits at the right, quiet: it is a reference, not a value
    anybody is being asked to act on. It is also what the empty box falls back
    to, which is why it is drawn at all. */
.hrsdef
{
    color: var(--ink-soft);
    text-align: right;
    white-space: nowrap;
}

/*  Both tables are one form each, so the two stack; keep them apart enough
    that the second heading does not read as a footnote to the first. */
.hrstable + .formactions
{
    margin-bottom: 26px;
}

/*  A LESSON'S LINK IN THE DETAIL LIST - 13 Sep 2026.

    A Teams or Webex address is 120 characters with no spaces in it, and a
    420px drawer has nowhere to break one: without this it pushes the panel
    sideways and takes the grid's scrollbar with it. `anywhere` rather than
    `break-all` so it still breaks at the slashes where it can, and only
    mid-token when it must. */
.detail-link a
{
    overflow-wrap: anywhere;
}

/*  THE BIN IN A DRAWER HEAD - 13 Sep 2026.

    It takes the `margin-left: auto` off the close button and carries it
    itself, so the pair sits at the right edge in the order bin-then-close.
    WITHOUT the adjacent-sibling rule below, both would have an auto margin
    and the free space would be SPLIT between them - the bin would float into
    the middle of the header with a gap on either side. */
.drawer-bin
{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    margin-left: auto;
    border-radius: var(--radius-s);
    color: var(--ink-soft);
    text-decoration: none;
}

.drawer-bin + .drawer-shut
{
    margin-left: 0;
}

/*  RED ONLY ON HOVER, and that is a decision rather than a shortage of
    colour. A permanently red icon in the corner of a panel a planner opens
    forty times a day reads as a warning about the LESSON - that something is
    wrong with it - which is the same misreading the permanently red overlap
    shade produced on 13 Sep. It turns red under the pointer, where the colour
    is about what the click will do. */
.drawer-bin:hover,
.drawer-bin:focus-visible
{
    background: color-mix(in srgb, var(--danger, #c0392b) 14%, transparent);
    color: var(--danger, #c0392b);
}

.drawer-bar
{
    margin-bottom: 10px;
}

.drawer-note
{
    margin: 0 0 10px;
    color: var(--ink-soft);
    font-size: 12px;
}

/*  Pushed to the bottom of the drawer, always in the same place, so it does
    not sit between the search box and the list it describes. */
.drawer-foot
{
    margin-top: auto;
    padding-top: 12px;
    border-top: 1px solid var(--line);
}

.drawer-capped
{
    font-weight: 600;
    color: var(--ink);
}

/*  Same rule as the pick set's rows, and for the same reason: .todo-item sets
    display, so it outranks the user-agent's [hidden]. */
.todo-item[hidden]
{
    display: none;
}

/*  THE RIGHT RAIL IS GONE, so the grid is not one half of a split any more.
    The class stays because the drawer is still a child of it, and because a
    rule removed is a rule somebody has to rediscover. */
body.admin .gridsplit
{
    display: block;
}

/*  AND LAST, BECAUSE ORDER IS THE ONLY THING SEPARATING THEM.

    The narrow-screen block further up already says the fold is hidden under
    900px - and then .lrail-fold's own `display: flex` was appended below it,
    at equal specificity, which in CSS means the later one wins. The chevron
    came back on a 760px screen, floating over a rail that has no width to
    give back. Repeated here, at the end, where nothing can be appended after
    it without noticing this note. */
@media (max-width: 900px)
{
    .lrail-fold
    {
        display: none;
    }
}

/* ------------------------------------------------------------------ */
/* Lists: the bar, the density, the filter                             */
/* ------------------------------------------------------------------ */

/*  A SEARCH BOX ON THE LIST TOO. The pick set fixed "stotina checkboxa";
    the lists had the same problem and nobody had said it out loud - 263
    lecturers, fourteen rows on a screen, and no way to Muševič except the
    scrollbar. Same partial, same script, same bargain: with no JavaScript
    the box is not drawn and the list is the whole list.

    See templates/_listbar.php. */
.editbar
{
    display: flex;
    align-items: center;
    gap: 12px;

    /*  IT WRAPS - 11 Sep 2026, and this is why.
    
        Mišo, on the bookings list the hour after the page-size control was
        added to it: "nema jos limitera". It was there, and the served HTML
        proved it - but this bar is search (320px) plus a button plus the size
        control plus the count, inside an .editwrap that has the left rail
        beside it. Without wrapping, a flex row that does not fit does not
        wrap: it overflows, and .perpick carries `margin-left: auto`, so the
        one control that gets pushed past the edge is exactly the one that was
        just added.
    
        .listtop has had this since it was written, for the same reason and in
        the same words - "a bar that overflows is a search box somebody cannot
        reach". Every bar in this application is that kind of row, so the rule
        belongs here and not on one of them. */
    flex-wrap: wrap;
    row-gap: 8px;
}

.listbar-search
{
    /*  Wide enough for a surname and a room code and no wider. A filter that
        stretches to the width of the page reads as the page's own search -
        for the whole school, not for the eighty rows underneath it. */
    flex: 0 1 320px;
    min-width: 0;
}

/*  The count sits BESIDE THE FILTER, not at the far end of the bar.

    margin-left:auto put it against the right edge of a 1130px page, a hand's
    width from the box whose result it reports - two numbers that change
    together, at opposite ends of the screen. Next to the box it reads as part
    of the search, which is what it is. */
.editbar .editcount
{
    font-variant-numeric: tabular-nums;
}

/*  Same rule as the pick set's rows and the drawer's, and for the same
    reason: a table row sets its own display, so it outranks the user agent's
    [hidden]. Three components have needed this now. */
.edittable tr[hidden],
.listtable tr[hidden]
{
    display: none;
}

/* ------------------------------------------------------------------ */
/* Density                                                             */
/* ------------------------------------------------------------------ */

/*  9px of padding on a row is right for a form and wrong for a list of
    eighty-four rooms: it put fourteen rows on a 950px screen, so finding
    anything meant scrolling past six screens of white. 6px puts twenty on the
    same screen and reads no harder - the room board went 38 to 22 for exactly
    this reason on 3 Sep.

    NOT LOWER THAN THIS. The rows are click targets, and 30px is about where a
    row stops being comfortable to hit with a trackpad. */
body.admin .edittable th,
body.admin .edittable td,
body.admin .listtable th,
body.admin .listtable td
{
    padding: 6px 12px;
}

/*  The heading row keeps its air: it is read once, and a heading tight against
    the first row reads as part of it. */
body.admin .edittable th,
body.admin .listtable th
{
    padding-top: 9px;
    padding-bottom: 9px;
}

/*  A row is a link now that the Edit button is gone from the end of it - so
    the whole row lights up under the pointer, not only the four words that
    happen to be an anchor. */
body.admin .edittable tbody tr:hover,
body.admin .listtable tbody tr:hover
{
    background: var(--surface-2);
}

/*  THE DEFAULT SAYS NOTHING, the exception is a chip. A column of eighty-four
    Yeses with three Nos in it hides the three. */
.chip-off
{
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

/* ------------------------------------------------------------------ */
/* The week jump                                                       */
/* ------------------------------------------------------------------ */

/*  A strip of fifty-two links became one box that takes either a week number
    or a date - templates/schedule.php says why. It sits between the arrows
    and the week label, which is where the reader is already looking. */
.weekjump
{
    display: flex;
    margin: 0;
}

.weekjump-box
{
    width: 132px;
    min-height: 28px;
    padding: 0 9px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-s);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 12.5px;
}

.weekjump-box:focus
{
    outline: 2px solid var(--brand);
    outline-offset: -1px;
    border-color: var(--brand);
}

/*  The placeholder is the whole instruction - "Week or date" - so it must not
    be the faintest thing in the bar. */
.weekjump-box::placeholder
{
    color: var(--ink-soft);
    opacity: 1;
}

/*  THE FOURTH COMPONENT TO NEED THIS, so it is written as a rule about the
    attribute rather than about one class.

    [hidden] { display: none } lives in the user-agent sheet as a plain
    attribute selector, so ANY class rule that sets display outranks it -
    .pickset-row, .todo-item, a table row, and now the group form's split
    field, which is a <label class="wide"> and therefore display:grid.

    Every one of them looked like a script that did nothing. */
[hidden]
{
    display: none !important;
}


/* ------------------------------------------------------------------ */
/* The mark on the sign-in                                             */
/*                                                                     */
/* WHY IT IS HERE AND NOT IN app.css. app.css is the shared sheet and  */
/* is copied verbatim into this tree by build-from-web.py; anything    */
/* written there for the administration door would either have to be   */
/* carried upstream into a public application that has no such door,   */
/* or be silently overwritten on the next build. This sheet is not     */
/* copied, so it is where admin-only rules belong.                     */
/*                                                                     */
/* IT SETS NO COLOUR. The squares carry their own four fills in the    */
/* markup because they ARE the mark; everything else here is size and  */
/* spacing, so the dark theme needs no rule of its own.                */
/* ------------------------------------------------------------------ */

.signin-mark
{
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;

    /*  Sits above the heading, not inside the card. The card's own top edge
        is the one accent this screen spends (see "ONE ACCENT" at the top of
        this file), and putting the mark inside it would give the eye two
        things to land on before it reaches the first field. */
    margin: 0 0 4px;
}

/*  THE ARTWORK ITSELF, 6 Sep 2026 - it was four squares drawn inline with the
    wordmark set in the page's own typeface, and it is now wtt-mark.png.

    240px WIDE AND NOT 330. The file is 330x95, which is wider than the 372px
    of card it would sit on once the padding is taken off - so it is scaled to
    240 and the height follows the ratio. `height: auto` with the intrinsic
    size still on the tag is what keeps the box reserved before the image
    lands AND lets the width rule win. */
.signin-logo
{
    display: block;
    width: 240px;
    height: auto;
    max-width: 100%;
}

/*
 * THE WHITE PLATE IS GONE - 12 Sep 2026. Mišo: "logo na login treba bit
 * transparentan - mozda malo zivlje boje - ali da pase i na dark i light
 * mode".
 *
 * The plate was here because the wordmark is slate on transparent and slate
 * on a dark ground cannot be read; a white rectangle was the cheapest way to
 * give it paper to sit on. It also looked like exactly that.
 *
 * There are two files now - see templates/login.php - so there is nothing to
 * hide behind and nothing to lift off the page. All that is left is choosing
 * which of the two to draw, and the rule below it is the whole of it.
 *
 * THE DEFAULT IS THE LIGHT FILE, in both directions: a browser that supports
 * neither the class nor the media query still gets a readable mark on the
 * light ground this application ships with.
 */
.signin-logo-dark
{
    display: none;
}

body.mode-dark .signin-logo-light
{
    display: none;
}

body.mode-dark .signin-logo-dark
{
    display: block;
}

@media (prefers-color-scheme: dark)
{
    body.admin:not(.mode-light) .signin-logo-light
    {
        display: none;
    }

    body.admin:not(.mode-light) .signin-logo-dark
    {
        display: block;
    }
}


/* ------------------------------------------------------------------ */
/* THE FOLD CONTROL, REDRAWN - 6 Sep 2026                              */
/*                                                                     */
/* Mišo, having used it: "malo mi sklanjanje i otvaranje (za sada      */
/* elipsasti neki kruzic sa strelicom - treba popravit) bude smooth".  */
/*                                                                     */
/* He is describing what it actually was. The button inherited the     */
/* rail ITEM's shape - 34px tall, 10px of side padding, a small        */
/* corner radius - and then got a fixed 40px width when the rail was   */
/* folded. A 40x34 rounded rectangle with a chevron in it is an        */
/* ellipse with an arrow, exactly as he says, and it read as a sixth   */
/* place to go rather than as a control.                               */
/*                                                                     */
/* It is now a 24px circle on the rail's outer edge, near the top:     */
/* the one shape every application that folds a rail uses, which is    */
/* the whole argument for it - a control nobody has to be taught costs */
/* nothing to keep.                                                    */
/*                                                                     */
/* THESE RULES COME LAST ON PURPOSE. Four earlier blocks in this file  */
/* say something about .lrail-fold, three of them by inheritance from  */
/* .lrail-item. Rather than unpick a shared rule that is right for the */
/* items, the button is defined once, here, at a specificity that      */
/* wins - and the earlier ones become dead weight rather than a trap.  */
/* ------------------------------------------------------------------ */

.lrail
{
    /*
     * THE RAIL'S POSITION IS NOT SET HERE, AND THAT IS THE WHOLE NOTE.
     *
     * This said `position: relative`, with the reason "the button hangs off
     * the right edge, so the rail is what it is positioned against" - which
     * is true and was already true: the rail is `position: sticky` two
     * thousand lines up, and a sticky box is a containing block for
     * absolutely-positioned children exactly like a relative one. The
     * declaration was unnecessary, and because this block is deliberately
     * LAST it won.
     *
     * SO THE LEFT RAIL HAS NOT BEEN STICKY SINCE THE FOLD BUTTON WAS REDRAWN
     * on 6 Sep 2026. Two symptoms, neither of which looks like this cause:
     *
     *   it scrolled away with the page, which reads as "the rail is just a
     *   column" rather than as a bug, and nobody had a tall enough screen of
     *   content to miss it;
     *
     *   and `top: var(--topbar-h)` went on being obeyed - relative offsets
     *   always are - so the rail was pushed 52px DOWN out of its own box
     *   without taking any layout space with it. On a short window that is
     *   52px of rail hanging below the shell, on top of the footer. Mišo,
     *   with the screenshot: "jedino nesto se sudara sa logom dole" - the
     *   Settings row and the wise timetable mark drawn one over the other.
     *
     * Measured at 1180x600: the shell ended at 492, the footer began at 506,
     * and the rail's box ran to 544.
     *
     * Nothing replaces it. If a future rule here ever needs a positioning
     * context, it already has one.
     */

    /*  SMOOTH, WHICH IS ONLY POSSIBLE WITH THE SCRIPT.

        Without JavaScript the fold is a link, a cookie and a page load -
        there is no width to animate because the new width arrives with a new
        document. admin.js intercepts the click, sets the same cookie itself
        and toggles the class in place, and THAT is what this transition is
        for. The no-script path still works and simply arrives folded.

        Width and padding both, because the folded rail changes both and
        animating one gives a rail that snaps narrow and then slides. */
    transition: flex-basis .18s ease, width .18s ease, padding .18s ease;
}

.lrail .lrail-fold
{
    position: absolute;

    /*  Level with the first heading, not with the first item: the eye that
        is looking for a fold control looks at the TOP of the edge. */
    top: 14px;

    /*  Half outside the rail, straddling its border. That is what makes it
        read as belonging to the edge rather than to the list - and it is
        why .lrail must not be the scroll container (see .lrail-scroll). */
    right: -12px;
    z-index: 3;

    display: flex;
    align-items: center;
    justify-content: center;

    /*  24px, and every one of these resets an inherited value rather than
        being decoration: the shared .lrail-item rule sets min-height 34,
        gap 10 and side padding, and .lrail-folded .lrail-fold sets width 40. */
    width: 24px;
    height: 24px;
    min-height: 0;
    flex: none;
    gap: 0;
    padding: 0;
    margin: 0;

    border-radius: 50%;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--ink-soft);
    box-shadow: var(--shadow-1);

    cursor: pointer;

    transition: background .15s ease, color .15s ease,
                border-color .15s ease, transform .15s ease;
}

.lrail .lrail-fold:hover
{
    color: var(--ink);
    border-color: var(--line-strong);
    background: var(--surface-2);
}

/*  A press that moves. The button is 24px and the click is the whole
    interaction, so the only feedback available is the button itself. */
.lrail .lrail-fold:active
{
    transform: scale(.92);
}

.lrail .lrail-fold:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/*  ONE CHEVRON, TURNED - not two icons.

    The template still renders the pointing-the-right-way path for the
    no-script case, and this rotation is what makes the SAME element turn
    when the script folds the rail in place. Rotating a glyph that is already
    the correct way round is a no-op, so the two paths cannot disagree. */
.lrail .lrail-fold .lrail-icon
{
    transition: transform .18s ease;
}

.lrail-folded .lrail-fold .lrail-icon
{
    transform: rotate(180deg);
}

/*  The items animate their own contents so the labels do not simply vanish
    the instant the class flips. */
.lrail-label
{
    transition: opacity .12s ease;
}

.lrail-folded .lrail-label
{
    opacity: 0;
}

/*  RESPECT THE SYSTEM SETTING. Somebody who has asked their machine for less
    motion has asked for it here too, and a rail is not the place to argue. */
@media (prefers-reduced-motion: reduce)
{
    .lrail,
    .lrail .lrail-fold,
    .lrail .lrail-fold .lrail-icon,
    .lrail-label
    {
        transition: none;
    }
}

/* ------------------------------------------------------------------ */
/* The bookings list                                                   */
/* ------------------------------------------------------------------ */

/*  The school's own colour for a booking type, as a dot before the name.

    A DOT AND NOT A FILLED CHIP. tbcolors holds whatever the school picked,
    including colours that no text is readable on - a chip would have to
    decide a foreground per row and would get it wrong on somebody's yellow.
    A 8px dot beside black text is legible whatever the colour is. */
.typedot
{
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 6px;
    vertical-align: baseline;

    /*  A hairline, so a white or very pale type is still a dot and not a
        gap in the sentence. */
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25);
}

/*  Paging: two links and a sentence, centred under the table. */
.pager
{
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    margin-top: 16px;
}

.pager-where
{
    font-size: 12.5px;
    color: var(--ink-soft);
}

/*  A button that is there to keep the row symmetrical and cannot be used.
    Drawn rather than hidden: a Previous that disappears on page 1 moves Next
    sideways, and a control that moves is a control people miss. */
.btn-off
{
    opacity: .45;
    cursor: default;
    pointer-events: none;
}


/*  AND THE NARROW-SCREEN RULE AGAIN, FOR THE SECOND TIME, and this note is
    the reason it is here twice.

    Under 900px the rail is not a rail - the layout stacks and there is no
    width to give back - so the fold button is hidden. That rule is written
    once at line ~1524, and the block above re-declared .lrail .lrail-fold at
    a HIGHER specificity (0,2,0 against 0,1,0), which puts the chevron back on
    a phone whatever the media query says. Exactly the fault the note up there
    describes, made again, one wave later, by the person who wrote the note.

    So it is repeated at the matching specificity, at the very end. Anything
    appended after this that touches .lrail-fold has to come past this comment
    first. */
@media (max-width: 900px)
{
    .lrail .lrail-fold
    {
        display: none;
    }
}


/* ------------------------------------------------------------------ */
/* The detail panel on a lesson - 6 Sep 2026                           */
/*                                                                     */
/* Mišo: "otvaranje details na eventu".                                */
/*                                                                     */
/* Same drawer as the outstanding list, because it is the same kind of */
/* thing: something you open, read, and close. It reuses .drawer       */
/* wholesale and only changes what is different - the width, and the   */
/* fact that its contents are pairs rather than rows.                  */
/* ------------------------------------------------------------------ */

/*  A CARD IS A LINK, so it has to stop looking like text and start looking
    like something you can press. The card already draws itself; all that is
    added here is the pointer, a focus ring, and a hint on hover. */
.card
{
    cursor: pointer;

    /*  <a> brings an underline and a link colour with it. The card decides
        its own colours from --card, and inheriting the brand blue would
        repaint every lesson in the week. */
    text-decoration: none;
    color: inherit;

    transition: box-shadow .12s ease, transform .12s ease;
}

.card:hover
{
    /*  A LIFT, NOT A COLOUR CHANGE. The card's fill is the school's own
        teaching-type colour and is the whole legend of the grid - a hover
        that tinted it would be a hover that lies about what kind of lesson
        this is. A shadow says "pressable" without touching the meaning. */
    box-shadow: var(--shadow-2);
    transform: translateY(-1px);
    z-index: 2;
}

.card:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 1px;
    z-index: 3;
}

/*
 * THE ONE THAT IS OPEN. A ring in the accent, not a fill: the card keeps its
 * own colour, and the reader needs to see WHICH card the panel beside them is
 * about without losing what kind of lesson it is.
 *
 * IT IS `card-here` AND NOT `card-open`, AND THAT IS A BUG FIX - 7 Sep 2026.
 *
 * Mišo: "jedan klik na karticu - details na desni ok. Isti klik - digne ghost
 * ekran preko" - a complete second copy of the application, scaled down, in a
 * scrollable overlay. Reported three times; I twice looked at the markup,
 * found it correct, and said so, which was true and useless.
 *
 * `card-open` was never only a state class. app.js - which is the PUBLIC
 * application's script, carried in by tools/build-from-web.py - hooks it as a
 * BEHAVIOUR:
 *
 *     ev.target.closest("a.card-open")   ->  preventDefault, then
 *     fetch(href + "&panel=1")           ->  slot.innerHTML = html
 *
 * In the public application that link points at /{school}/event/{id}, where
 * `panel=1` renders the detail panel alone. In the administration application
 * the same class landed on the grid card whose drawer is open, and its href is
 * the SCHEDULE page. Nothing there knows what `panel=1` means, so the server
 * answered with the whole page - topbar, rail, grid and all - and the script
 * dropped that entire document into a <dialog>.
 *
 * Which is exactly why it took two clicks. The first click is on a card with
 * no such class: no handler matches, the link navigates, the drawer opens.
 * The card is then re-rendered WITH the class, so the second click - the one
 * that should have closed the drawer - matched the public app's dialog
 * handler instead.
 *
 * The fix is the name. `here` is what this codebase already calls "the one you
 * are standing on" (viewpick-here), it says what the class means, and no
 * script hooks it.
 *
 * THE TRAP IS GENERAL, so it is written down here rather than in a commit
 * message: app.js is a foreign script loaded into these pages, and any class
 * IT treats as a hook is a name this application may not use for a state.
 * Before adding a class to something clickable, grep app.js for it.
 */
.card-here
{
    box-shadow: 0 0 0 2px var(--brand), var(--shadow-2);
    z-index: 3;
}

/*  Narrower than the outstanding drawer. That one is a list of hundreds and
    wants the room; this is one lesson's worth of pairs, and a 520px column
    of two-word values reads as a form somebody forgot to fill in. */
.drawer-detail
{
    max-width: 380px;
}

/*  The subject, at the top, in the size the eye lands on first. */
.detail-what
{
    margin: 0;
    font-size: 16px;
    font-weight: 650;
    line-height: 1.3;
    color: var(--ink);
}

.detail-kind
{
    margin: 2px 0 14px;
    font-size: 13px;
    color: var(--ink-soft);
}

/*  The school's own code for a subject or a room, set apart from the name
    rather than run into it - "1397" after a title is a number people read
    as part of the title unless something says otherwise. */
.detail-code
{
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: var(--radius-s);
    background: var(--surface-2);
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}

/*  TWO COLUMNS, LABELS NARROW AND FIXED.
    A definition list defaults to the label above the value, which doubles
    the panel's height for no gain: these labels are one word. */
.detail
{
    display: grid;
    grid-template-columns: 92px 1fr;
    gap: 7px 12px;
    margin: 0;
    font-size: 13px;
}

.detail dt
{
    color: var(--ink-soft);

    /*  Not bold. The label is the quiet half of the pair - the value is what
        somebody opened the panel to read. */
    font-weight: 400;
}

.detail dd
{
    margin: 0;
    color: var(--ink);

    /*  A long list of groups wraps inside its own half rather than pushing
        the panel wider. */
    min-width: 0;
    overflow-wrap: anywhere;
}

.detail-sub
{
    display: block;
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--ink-soft);
}

/*  The duration grammar - "1-16:2+1". Monospaced because it is a string with
    a syntax, and the difference between 2+1 and 2-1 is the whole meaning. */
.detail-plan
{
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    padding: 1px 5px;
    border-radius: var(--radius-s);
    background: var(--surface-2);
}

/*  The GUID, at the bottom, quiet and selectable. */
.detail-id
{
    margin: 16px 0 0;
    padding-top: 10px;
    border-top: 1px solid var(--line);
    font-size: 11px;
    color: var(--ink-soft);
    overflow-wrap: anywhere;
    user-select: all;
}

.detail-id code
{
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}


/* ------------------------------------------------------------------ */
/* APPEARANCE: the reader's own accent, and light or dark              */
/*                                                                     */
/* Mišo, 6 Sep 2026: "a tema - pa nije po skoli - bas u tom je fora -  */
/* skola ima ono sto po defaultu od nas dobije ... Ali poslije neka    */
/* bude na useru - jer iz iskustva znamo da se bas useri individualno  */
/* vole igrat sa tim."                                                 */
/*                                                                     */
/* THIS IS WHY THE WHOLE APPLICATION WAS BUILT ON TOKENS. admin.css    */
/* opens with "IT INVENTS NO COLOURS. Every value comes from app.css's */
/* tokens", and it has been kept true through five waves. The reward   */
/* is here: an accent is TWO custom properties, and every button,      */
/* focus ring, rail marker, card spine and sign-in edge in the         */
/* application follows without one further rule.                       */
/*                                                                     */
/* WHY THE CLASS IS ON <body> AND NOT ON :root. bodyClass is what the  */
/* shell already carries (see Router and templates/_head.php), and     */
/* body is where app.css reads --bg for the page background - so       */
/* redefining a token on body reaches everything that matters. html    */
/* paints nothing of its own.                                          */
/* ------------------------------------------------------------------ */

/*  --brand is the accent; --brand-ink is the darker one links use, so the two
    move together and a link never stays blue on a red page. Every pair below
    is at roughly one lightness, so white text sits on all eight and the
    layout does not change weight when the colour does. */

/*  --accent IS THE SECOND COLOUR, and it exists for exactly one job: telling
    a booking apart from a lesson on a control eighteen pixels wide. See
    .slotadd-book. It was READ there and DEFINED NOWHERE until 10 Sep 2026, so
    every school got the same hard-coded #a2601f whatever theme it had chosen -
    which is the fault tools/dead-tokens.sh was written to catch, and it caught
    this one.

    IT IS NOT A SHADE OF THE BRAND. A booking icon in a lighter brand would be
    the same icon; the two have to differ in HUE. So each accent below is
    chosen against its own brand, and the rule is temperature: a cool theme
    gets a warm accent, a warm theme gets a cool one. clay, amber and red are
    the warm three and they are the reason this is a table and not one value -
    a single warm accent would have been invisible on precisely those three.

    Each is at roughly the brand's own lightness, so white sits on it and the
    icon does not change weight when the school changes theme. */

/*  THE DEFAULT FIRST, AND THE ORDER IS THE WHOLE POINT.

    `body.admin` and `body.theme-teal` are the same specificity - one element,
    one class - so between them the LATER rule wins, and a themed page carries
    both classes. Put after the table below, this block would have overridden
    every theme and handed all eight schools the same accent: the exact fault
    being fixed, reintroduced by the fix. Before it, it is what it says it is -
    the value for a page that has no theme class at all.

    The same warm tone the old fallback used, so nothing that looks right today
    changes colour on the day this file is deployed. */
body.admin
{
    --accent: #a2601f;
}

body.theme-teal   { --brand: #0e8a80; --brand-ink: #0a6a62; --accent: #b5721a; }
body.theme-blue   { --brand: #2f6fed; --brand-ink: #1d4ed8; --accent: #b5721a; }
body.theme-sage   { --brand: #4f7a4a; --brand-ink: #3c5e38; --accent: #a2601f; }
body.theme-navy   { --brand: #2b4a7d; --brand-ink: #1f3a63; --accent: #b5721a; }
body.theme-clay   { --brand: #8a6a4f; --brand-ink: #6d523c; --accent: #0e7c74; }
body.theme-amber  { --brand: #b5721a; --brand-ink: #8e5813; --accent: #0e7c74; }
body.theme-red    { --brand: #c0392b; --brand-ink: #99291d; --accent: #0e7c74; }
body.theme-plum   { --brand: #8e3f78; --brand-ink: #70305e; --accent: #b5721a; }

/*  TWO MORE, 13 Sep 2026, in the two directions the first eight left empty.
    Both sit at the same lightness as the rest, which is what lets every
    swatch carry white text behind the same layout.

    violet  a blue-purple. plum is magenta and navy is blue; nothing was
            between them.
    slate   a neutral grey-blue, for somebody who wants the accent to stop
            shouting. Its --accent is the warm one, because a neutral brand
            with a neutral accent has nothing left to mark anything with. */
body.theme-violet { --brand: #6b4fb5; --brand-ink: #543d91; --accent: #b5721a; }
body.theme-slate  { --brand: #4a5b68; --brand-ink: #384652; --accent: #b5721a; }

/*  THE GLOW BEHIND THE PAGE FOLLOWS THE ACCENT.

    app.css paints the body with a radial gradient whose colour is the wise
    blue written out as a literal - rgb(47 111 237 / .07) - because when it
    was written there was one accent. On a red page that literal is a blue
    haze in the top-left corner, and it is exactly the kind of thing somebody
    sees without being able to name.

    Redeclared here from --brand instead. The attribute selector catches all
    eight without eight more rules; color-mix does what the literal did. */
body[class*="theme-"]
{
    background:
        radial-gradient(1200px 400px at 20% -10%,
            color-mix(in srgb, var(--brand) 8%, transparent), transparent 60%),
        var(--bg);
}

/* ------------------------------------------------------------------ */

/*  LIGHT AND DARK BY HAND.
 *
 *  app.css already answers prefers-color-scheme, and that stays the default -
 *  somebody who has never opened the picker gets what their machine asked
 *  for, which they have already chosen once. These two classes are for the
 *  person whose machine says one thing and who wants the other HERE.
 *
 *  THE DARK VALUES ARE app.css's OWN, copied rather than invented, so the
 *  hand-set dark and the automatic dark are the same dark. If that block ever
 *  changes, this one has to change with it - and this sentence is the only
 *  thing that will remind anybody.
 *
 *  color-scheme is not decoration either: without it the browser paints the
 *  controls IT owns - select popups, scrollbars, autofill, <dialog> text - in
 *  light colours, and the tokens below would be the only dark thing on the
 *  screen.
 */
body.mode-dark
{
    --bg:          #101318;
    --surface:     #181c23;
    --surface-2:   #1f242d;
    --ink:         #e8eaf0;
    --ink-soft:    #9aa3b2;
    --line:        #262c36;
    --line-strong: #3a4250;

    --shadow-1: 0 1px 2px rgb(0 0 0 / .5);
    --shadow-2: 0 4px 12px rgb(0 0 0 / .5);
    --shadow-3: 0 12px 32px rgb(0 0 0 / .6);

    color-scheme: dark;
}

body.mode-dark select
{
    color-scheme: dark;
}

/*  LIGHT ON A DARK MACHINE. The light values are :root's own, restated,
    because they are only the default until the media query fires - and on a
    machine set to dark it has already fired by the time this class is read. */
body.mode-light
{
    --bg:          #f4f6fa;
    --surface:     #ffffff;
    --surface-2:   #eef1f6;
    --ink:         #16181d;
    --ink-soft:    #5b6472;
    --line:        #e3e7ee;
    --line-strong: #c9d0da;

    --shadow-1: 0 1px 2px rgb(16 24 40 / .06), 0 1px 3px rgb(16 24 40 / .10);
    --shadow-2: 0 4px 12px rgb(16 24 40 / .10), 0 2px 4px rgb(16 24 40 / .06);
    --shadow-3: 0 12px 32px rgb(16 24 40 / .16);

    color-scheme: light;
}

body.mode-light select
{
    color-scheme: light;
}

/*  ONE PLACE THE TOKENS DO NOT REACH, and it has to be said.

    schedule.css mixes a lesson's card against #fff by name -
    `color-mix(in srgb, var(--card) 14%, #fff)` - so on a dark page every card
    is a pale block on a dark grid. That is a bug in dark mode that predates
    the switch; the switch only makes it reachable without changing the
    operating system. Fixed here rather than there because schedule.css is
    about the grid's geometry and this is about the scheme. */
body.mode-dark .card
{
    background: color-mix(in srgb, var(--card) 22%, var(--surface));
}

/*  AND THE SAME FIX FOR THE AUTOMATIC DARK, which is where the bug has been
    living all along - a machine set to dark has always drawn this grid with
    pale cards, and nobody noticed because nobody develops in dark mode.

    :not(.mode-light) so the manual light override still wins on a dark
    machine: without it, choosing Light would give a light page with dark
    cards, which is the same bug wearing the other coat. */
@media (prefers-color-scheme: dark)
{
    body.admin:not(.mode-light) .card
    {
        background: color-mix(in srgb, var(--card) 22%, var(--surface));
    }
}

/* ------------------------------------------------------------------ */
/* The picker itself                                                   */
/* ------------------------------------------------------------------ */

/*  It borrows .langpick's pop-over whole - see templates/_themepick.php - so
    all that is left is the grid of dots. */
.themepick-pop
{
    min-width: 210px;
}

/*  FIVE ACROSS, TWO ROWS - 13 Sep 2026. It was four across for eight
    colours; Mišo asked for two more "desno", so the row got wider rather than
    the grid taller. Ten in 4-wide would have been 4+4+2, a last row half
    empty, which is exactly the shape a person reads as unfinished. */
.themepick-grid
{
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
    padding: 4px 2px 10px;
}

/*  The one item in the light/dark list that takes the whole width, so the two
    that are a pair - Light and Dark - sit beside each other on the next line.
    .langpick-list is a two-column grid; without this the three items fill it
    in order and Dark drops to a row of its own. */
.themepick-wide
{
    grid-column: 1 / -1;
}

/*  A DOT, 22px, WITH ITS OWN COLOUR PASSED IN.

    The colour comes from the template as an inline --swatch, because a swatch
    shows the colour it will apply and that colour only exists once the class
    is on the body. Eight more CSS rules whose only content is a repeat of the
    eight hex values above would be a second list to keep in step. */
.themepick-dot
{
    display: block;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--swatch, var(--brand));

    /*  A hairline inside, so a pale swatch is still a disc and not a hole. */
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / .18);

    transition: transform .12s ease, box-shadow .12s ease;
}

.themepick-dot:hover
{
    transform: scale(1.12);
}

/*  THE ONE THAT IS ON, marked with a ring OUTSIDE the dot rather than a tick
    inside it. A tick has to be light or dark to be legible and would be wrong
    on half the swatches; a ring in the ink colour is legible on all eight and
    on both schemes. */
.themepick-dot.is-active
{
    box-shadow:
        inset 0 0 0 1px rgb(0 0 0 / .18),
        0 0 0 2px var(--surface),
        0 0 0 4px var(--ink-soft);
}

.themepick-dot:focus-visible
{
    outline: 2px solid var(--ink);
    outline-offset: 3px;
}


/* ------------------------------------------------------------------ */
/* WHERE THE ACCENT ACTUALLY SHOWS - 6 Sep 2026                        */
/*                                                                     */
/* Mišo, having tried the picker: "teme rade odlicno - jedino zaista   */
/* ne promijene mnogo - a i recimo oznaka gore lijevo ostane uvijek    */
/* plava".                                                             */
/*                                                                     */
/* He is right, and the reason is in this file's own opening note:     */
/* "ONE ACCENT, SPENT ONCE PER SCREEN. The sign-in card's top edge,    */
/* the pending panel's left edge, and the focused control."            */
/*                                                                     */
/* That restraint was correct while there was ONE accent and it was    */
/* ours. A colour nobody chose should be quiet. But a colour somebody  */
/* has just picked out of eight has to be visible, or the picker is a  */
/* control that appears not to work - which is exactly what he saw.    */
/*                                                                     */
/* So the accent now reaches the surfaces that are ABOUT THE FRAME -   */
/* the bar under the header, the rail's current entry, table headings, */
/* counts, the drawer's edge - and still does not touch anything that  */
/* carries meaning of its own. That line is the whole design and it is */
/* worth stating: a lesson's card colour is the school's teaching type */
/* and a booking's dot is the school's tbcolors, so neither of them    */
/* moves. Recolouring those would make the timetable lie.              */
/* ------------------------------------------------------------------ */

/*  A HAIRLINE UNDER THE TOP BAR, and it is the biggest single change.

    It is 2px across the whole window, it sits directly under the one thing
    on every screen, and it costs no height that was not already a border.
    This is the line that makes a chosen colour read as "the application is
    green now" rather than as "one button is green". */
body.admin .topbar
{
    border-bottom: 2px solid var(--brand);
}

/*  The screen's own name in the bar, and the school chip beside the mark. */
body.admin .weeklabel
{
    color: var(--brand-ink);
}

body.admin .chip-school
{
    color: #fff;
    background: var(--brand);
    border-color: transparent;
}

/*  THE MARK ITSELF STAYS AS IT IS, and this note is why.

    The four squares are #768ed2, #aabae5, #e84c67 and #e2da66 - they are not
    tinted brand blue, they ARE the mark, the same four the phone
    application's launcher icon uses. A mark that answered to a colour picker
    would be eight marks, and the one place a product must look like itself is
    the corner.

    What DOES follow the accent is the word beside it, which is type and not
    artwork - so the corner reads as themed without the logo being redrawn. */
body.admin .brand-name b
{
    color: var(--brand-ink);
}

/*  THE CURRENT ENTRY IN THE RAIL, tinted rather than merely edged. The 3px
    spine was already the accent; at 210px wide, on the left edge of the
    screen, three pixels is not a colour anybody sees. */
body.admin .lrail-here
{
    background: color-mix(in srgb, var(--brand) 10%, var(--surface));
}

body.admin .lrail-here .lrail-label
{
    color: var(--brand-ink);
}

/*  TABLE HEADINGS. Every list in this application is a table and the heading
    row is on all of them, so this is the accent's second-widest appearance
    after the bar. Tinted, not filled: a solid accent bar over eighty rows
    fights the rows for attention, and the rows are the point. */
body.admin .listtable thead th,
body.admin .edittable thead th,
body.admin .plantable thead th
{
    background: color-mix(in srgb, var(--brand) 7%, var(--surface));
    border-bottom: 2px solid color-mix(in srgb, var(--brand) 45%, var(--line));
}

/*  THE COUNT BESIDE A SEARCH BOX - and the ink has to be the one that is
    legible ON the accent, not the one made OF it.

    Mišo, 6 Sep 2026, on a green pill with dark green digits: "crni text...".

    This said `--brand-ink`, which is a darker SHADE of the accent, meant for
    accent-coloured text on white paper. The base .editcount fills the pill
    with the accent itself, so the two met and the number all but vanished -
    the same mistake the themed drawer made with --ink-soft, one class along.

    --on-brand is the token each theme declares as "what is readable on my
    accent". It is white today for all eight; the point is that the day a pale
    one is added, this line is already right. */
body.admin .editcount
{
    color: var(--on-brand);
}

/*  A DRAWER'S LEADING EDGE. It is the one element that arrives over the top
    of the page, so the edge that says "this is on top" may as well say which
    application it belongs to. */
body.admin .drawer
{
    border-left: 3px solid var(--brand);
}

/*  The chosen swatch's ring, so the picker itself is themed while it is open
    and the choice is visible the instant it is made. */
body.admin .themepick-dot.is-active
{
    box-shadow:
        inset 0 0 0 1px rgb(0 0 0 / .18),
        0 0 0 2px var(--surface),
        0 0 0 4px var(--brand);
}

/*  DARK MODE NEEDS MORE OF IT, NOT LESS. Every mix above is against
    --surface, so on a dark page a 7% tint of a mid-tone accent is nearly
    invisible. The proportions go up rather than the colours changing, so
    there is still one palette and one set of decisions. */
body.mode-dark .lrail-here
{
    background: color-mix(in srgb, var(--brand) 22%, var(--surface));
}

body.mode-dark .listtable thead th,
body.mode-dark .edittable thead th,
body.mode-dark .plantable thead th
{
    background: color-mix(in srgb, var(--brand) 16%, var(--surface));
}

@media (prefers-color-scheme: dark)
{
    body.admin:not(.mode-light) .lrail-here
    {
        background: color-mix(in srgb, var(--brand) 22%, var(--surface));
    }

    body.admin:not(.mode-light) .listtable thead th,
    body.admin:not(.mode-light) .edittable thead th,
    body.admin:not(.mode-light) .plantable thead th
    {
        background: color-mix(in srgb, var(--brand) 16%, var(--surface));
    }
}

/* ------------------------------------------------------------------ */
/* The paged list bar                                                  */
/* ------------------------------------------------------------------ */

/*  It is _listbar's row plus two controls, so it inherits .editbar and adds
    only what is new.

    THE WRAPPING MOVED UP to .editbar on 11 Sep - it was here first, for the
    reason written there, and the bookings bar needed it too. Nothing is left
    for this rule to add, and an empty rule that keeps its comment is how the
    next reader learns where the behaviour actually lives. */

.perpick
{
    display: flex;
    align-items: center;
    gap: 6px;

    /*  Pushed to the far end, away from the search box and its count. Those
        two belong together - one is the answer to the other - and the page
        size is a third, unrelated thing. */
    margin-left: auto;
}

.perpick-label
{
    font-size: 12px;
    color: var(--ink-soft);
    white-space: nowrap;
}

.perpick-box
{
    /*  Wide enough for "All" in thirteen languages and no wider: this control
        must not compete with the search box for the bar's width. */
    width: auto;
    min-width: 72px;
    padding-right: 6px;
    font-variant-numeric: tabular-nums;
}

/*  WHICH PROGRAMME'S TREE - 6 Sep 2026. Mišo: "programi su nepregledni - neka
    budu u combo boxu pa onda se dole otvore njegove godine i smjerovi".

    Wide, because a programme name is a sentence - "Univerzitetni študijski
    program Tehniška varnost - 1.st" - and a picker that clips it leaves twelve
    rows that all begin the same way.

    IT TAKES WHAT IT NEEDS AND NO MORE. Mišo: "i taj text priblizi programima
    (to je broj programa)". Growing to fill the bar pushed the count of
    programmes to the far right of a 1130px page, a hand's width from the
    control it counts - two things about the same list at opposite ends of the
    screen. `flex-grow: 0` and the number sits where it belongs. */
body.admin .progpick
{
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: 0 1 560px;
    min-width: 0;
}

body.admin .progpick-box
{
    flex: 1 1 auto;
    min-width: 0;
    max-width: 640px;
}


/* ------------------------------------------------------------------ */
/* A NAME IN THE DETAIL PANEL IS A DOOR                                */
/* 6 Sep 2026                                                          */
/* ------------------------------------------------------------------ */

/*  Mišo: "sve te nazive u details bi imao polinkane na entitete koji se onda
    otvore iz lijevog raila".

    UNDERLINED ON HOVER, NOT ALWAYS. The panel is a dozen name/value pairs and
    most of the values are now links; underlining all of them would turn a
    quiet reference into a page of blue rules. The pointer and the hover say
    it, which is what the outstanding rows already do.

    COLOUR COMES FROM THE PANEL. Inside the themed drawer --ink is the panel's
    own white (see the token switch), so a link there is white on the accent
    and legible without this block knowing which theme is on. */
body.admin .detail-link
{
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, currentColor 35%, transparent);
}

body.admin .detail-link:hover
{
    border-bottom-color: currentColor;
}

/*  THE SECOND DOOR: this entity's own week, in the grid behind the panel. A
    small arrow rather than a second name, because the name is the thing being
    read and two links of equal weight on one line is a choice nobody wants to
    make while reading. */
body.admin .detail-week
{
    margin-left: 4px;
    padding: 0 4px;
    border-radius: var(--radius-s);
    color: inherit;
    opacity: .55;
    text-decoration: none;
}

body.admin .detail-week:hover
{
    opacity: 1;
    background: color-mix(in srgb, currentColor 16%, transparent);
}

/*  Several names on one line, each with its own two doors. Wrapping rather
    than clipping: this is the panel, where the whole answer belongs - the
    CARD is the place that clips, because a card is a thumbnail. */
body.admin .detail-people
{
    display: flex;
    flex-wrap: wrap;
    gap: 2px 10px;
}

body.admin .detail-person
{
    white-space: nowrap;
}

/*  THE SIZE, BETWEEN THE ARROWS - 6 Sep 2026. Mišo: "taj izbor moze bit
    između strelica isto". It sits in the middle of the pager, so the two
    jumps stay at the ends where the hand looks for them. */
body.admin .pager-per
{
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0 4px;
}

/*  Which rows of the whole list this page is - "51-75". Beside the page
    number, quieter than it, because it answers a second question. */
.pager-rows
{
    margin-left: 8px;
    font-variant-numeric: tabular-nums;
    /*  no opacity (wave 9, UX9-13): the soft ink at .75 was 4.4:1 on the dark
        panel; "1-25" is information, and the soft ink alone already sets it back. */
}


/*  THE COUNT INSIDE A BUTTON INHERITS THE BUTTON - 6 Sep 2026.

    Mišo: "not placed ima pored sebe broj koliko ih je - ali taj broj se ne
    vidi unutar nekih tema".

    A few rules above, `body.admin .editcount` was given var(--brand-ink) so
    the count beside a search box picks up the accent. That is right where the
    count sits on the page's own background - and wrong the moment it sits
    INSIDE something with a background of its own, which is exactly where the
    "Not placed" count lives: in a button, and in a button that fills with the
    accent when the drawer is open. Accent ink on an accent fill is an
    invisible number.

    So the general rule stands and the count inherits wherever it is on top of
    something. `currentColor` rather than a named token, because the thing it
    is inside has already decided what is legible against itself - a filled
    button is white text, an outline button is ink - and inheriting is the
    only answer that is right for both without listing them. */
body.admin .btn .editcount,
body.admin .storebadge .editcount,
body.admin .viewpick-todo .editcount
{
    color: inherit;

    /*  And a plate behind it, so the number reads as a count and not as part
        of the label. Mixed from the surrounding text colour, so it works on a
        filled button and an outline one, in light and in dark, without
        knowing which it is. */
    background: color-mix(in srgb, currentColor 16%, transparent);
    border-radius: 999px;
    padding: 0 6px;
    min-width: 18px;
    text-align: center;
}

/*  THE TWO COUNTS IN THE GRID'S TOOLBAR ARE FILLED - 6 Sep 2026.
 *
 *  Mišo, looking at them: "ovdje bi da na oba polja boju teme i text bijeli".
 *
 *  The rule above is a tinted plate of whatever colour surrounds it, which is
 *  right for a count that has to survive being inside anything - and it is
 *  too quiet for these two. They sit in the toolbar over the page's own pale
 *  surface, so the plate came out grey on grey on the "Not placed" button and
 *  accent on accent inside the find chip, whose text is the accent when open.
 *
 *  These two know exactly what they are on, so they can say it: the theme's
 *  colour, filled, with the ink that theme has already declared legible on
 *  itself. A count is a number somebody is looking for on a busy screen, and
 *  it should read like one.
 *
 *  AFTER the rule above and at the same specificity, so source order decides.
 *  Moving either block past the other silently undoes this. */
body.admin .viewpick-todo .editcount,
body.admin .viewfind-sum .editcount
{
    background: var(--brand);
    color: var(--on-brand);
    font-weight: 600;
}


/* ------------------------------------------------------------------ */
/* TABS - ONE THING AT A TIME, ACROSS THE TOP                          */
/* 6 Sep 2026                                                          */
/* ------------------------------------------------------------------ */

/*  Mišo: "idemo sa tim tabovima kao sto si rekao frontalno - gdje god imamo
    vise ekrana".

    A ROW, NOT A BOX. The tabs sit on the same rule that separates them from
    the content, so the active one reads as continuous with what is below it -
    which is the whole visual claim a tab makes. Anything heavier competes
    with the tables underneath, and those are what somebody came to read. */
/*  A WRAPPED ROW READ AS A TREE - 13 Sep 2026, and this is the fix.

    Mišo, looking at the Tools screen's seven tabs over two lines: "ovaj tab
    nije odmah jasan… do sada sam mislio da su donje opcije ustvari dio neke
    gornje… ustvari su sve odvojene."

    He is right and the reason is the original design: an inactive tab was
    plain text with a transparent border, so on ONE line the spacing said
    "peers" and on TWO it said nothing at all - and a short second line under a
    long first line is the shape of a sub-menu in every application there has
    ever been.

    THE ANSWER IS TO GIVE EVERY TAB THE SAME FOOTPRINT, not to stop the row
    wrapping. Seven options are seven options; hiding three of them behind a
    "more" would be worse than the confusion. So each one gets its own faint
    outline: on a single-line row it is a quieter version of what the active
    tab already has, and on a wrapped row it is what says the second line is
    not subordinate to the first.

    The gap grows with it. At 2px the boxes touched and read as one segmented
    control, which is a different wrong thing. */
body.admin .tabrow
{
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    margin: 0 0 16px;
    border-bottom: 1px solid var(--line);
}

body.admin .tab
{
    padding: 7px 16px;

    /*  WAS `1px solid transparent`. The transparent border reserved the space
        so the active tab did not jump; now it is drawn, faintly, and does the
        second job as well. --line-strong rather than --line so it survives the
        dark scheme, where --line is nearly the surface it sits on. */
    border: 1px solid var(--line-strong);
    border-bottom: 0;
    border-radius: 6px 6px 0 0;

    /*  Sits ON the rule, so the active tab can cover it. */
    margin-bottom: -1px;

    color: var(--ink-soft);
    font-size: 13px;
    text-decoration: none;
    white-space: nowrap;
}

body.admin a.tab:hover
{
    background: var(--surface-2);
    color: var(--ink);
}

/*  THE ONE YOU ARE ON covers the rule beneath it - the page's own surface
    running up into the tab is what says "this is the same thing". The accent
    line on top is the second signal, for anybody who cannot see the first. */
body.admin .tab-here
{
    background: var(--surface);
    border-color: var(--line);
    border-top: 2px solid var(--brand);
    padding-top: 6px;
    color: var(--ink);
    font-weight: 600;
}

/*  NOT AVAILABLE YET, and drawn anyway. A tab that appears after the first
    save is a screen that changes shape under somebody; this one is visibly
    there, visibly dead, and its title says why. `default` rather than
    `not-allowed`: the reader has done nothing wrong. */
body.admin .tab-off
{
    color: var(--ink-soft);
    opacity: .5;
    cursor: default;
}


/* ------------------------------------------------------------------ */
/* TODAY, THE WEEKEND, THE DATE, AND WHERE THE CURSOR IS               */
/* 6 Sep 2026                                                          */
/* ------------------------------------------------------------------ */

/*  THE DATE BESIDE THE WEEKDAY. Mišo: "pored imena MON, TUE, dajmo jos datume
    kratko - 12th Sep na primjer - to su mi rekli da pomaze mnogo".

    IT WAS STACKED UNTIL 12 Sep 2026, on the grounds that "MON 12 Sep" across
    a column that can be 90px wide at seven days on a laptop would wrap in the
    middle of a date. True - and the price was a whole line of vertical space
    above the grid on every screen, including the wide ones, to prevent
    something that happens on the narrow ones. Mišo: "neka budu desno od imena
    iza zareza, da ustedimo jednu liniju gore".

    A ROW THAT MAY WRAP, which is neither of the two, and better than both.
    One line wherever it fits - nearly always - and where it genuinely does
    not, the weekday and the date fall onto two lines exactly as they used to.
    The narrow case is not made worse; it just stops being everybody's case.

    `column-gap` and not a space in the markup: the gap disappears with the
    wrap, so the second line starts hard against the left of nothing rather
    than indented by a stray space. The COMMA is written into the weekday in
    schedule.php, so a wrapped heading reads "MON," / "12 Sep" and a week with
    no date at all prints no comma. */
body.admin .grid-dayname
{
    display: flex;
    flex-flow: row wrap;
    align-items: baseline;
    justify-content: center;
    column-gap: .3em;
    line-height: 1.15;
}

body.admin .grid-daydate
{
    font-size: 10.5px;
    font-weight: 400;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}

/*  TODAY. An accent underline on the heading and the faintest wash down the
    column - not a filled header, which would compete with the cards for the
    one strong colour on the screen.

    The wash is 4%, which is on the edge of visible and is meant to be: it has
    to be findable when you look for it and invisible when you are reading a
    card on top of it. */
body.admin .grid-dayname.grid-today
{
    color: var(--brand-ink);
    font-weight: 650;
    box-shadow: inset 0 -2px 0 var(--brand);
}

body.admin .grid-dayname.grid-today .grid-daydate
{
    color: var(--brand-ink);
}

body.admin .grid-day.grid-todaycol
{
    background: color-mix(in srgb, var(--brand) 4%, transparent);
}

body.mode-dark .grid-day.grid-todaycol
{
    background: color-mix(in srgb, var(--brand) 10%, transparent);
}

/*  THE WEEKEND, when this reader asked for it. Grey and not accent: Saturday
    is not important, it is different, and the accent means important
    everywhere else on the screen. */
body.admin .grid-day.grid-weekend
{
    background: color-mix(in srgb, var(--ink) 4%, transparent);
}

body.admin .grid-dayname.grid-weekendhead
{
    color: var(--ink-soft);
}

/*  Today wins over the weekend when both apply - it is a Saturday in the
    week somebody is actually in, and "which column is today" is the more
    urgent of the two questions. Declared after, at equal specificity. */
body.admin .grid-day.grid-weekend.grid-todaycol
{
    background: color-mix(in srgb, var(--brand) 5%, transparent);
}

/* ------------------------------------------------------------------ */

/*  WHERE THE CURSOR IS, and the whole point is that it is FAINT.

    Mišo: "sasvim lagano ... da sam siguran gdje sam - jer ce slijedeci klik
    mozda biti - idi u not placed rail". It is a confirmation, read out of the
    corner of the eye while the eye is on the grid; a strong mark would pull
    the eye off the thing being pointed at, which is the opposite of helping.

    Added and removed by admin.js. With no script nothing here ever matches
    and the grid is what it was. */
body.admin .grid-hour.is-cursor
{
    color: var(--brand-ink);
    font-weight: 650;
    background: color-mix(in srgb, var(--brand) 10%, transparent);
    border-radius: var(--radius-s);
}

/*  THE HALF HOURS LIGHT UP TOO, and they carry no label - so on those rows
    the mark IS the answer. Without this the cursor would appear to snap to
    the hour, and somebody about to place a lesson at 10:30 would not believe
    the screen. */
body.admin .grid-hour.grid-hour-half.is-cursor::after
{
    content: "·";
    display: block;
    text-align: right;
    padding-right: 4px;
    color: var(--brand);
}

body.admin .grid-dayname.is-cursor
{
    background: color-mix(in srgb, var(--brand) 9%, transparent);
    border-radius: var(--radius-s) var(--radius-s) 0 0;
}

@media (prefers-reduced-motion: reduce)
{
    /*  No transition on either - they are already instant, and this is the
        rule that stops somebody adding one later without thinking. */
    body.admin .grid-hour.is-cursor,
    body.admin .grid-dayname.is-cursor
    {
        transition: none;
    }
}


/* ------------------------------------------------------------------ */
/* The settings screen, and the gear that opens it                     */
/* ------------------------------------------------------------------ */

/*  The gear is a link and its two neighbours are <summary> elements, so it
    has to be made to look like them rather than inheriting it. Square, the
    same height as a button, icon centred. */
.topbar-icon
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-h);
    padding: 0;
    color: var(--ink-soft);
}

.topbar-icon:hover
{
    color: var(--ink);
}

/*  Marked while you are ON the settings screen - the same courtesy the left
    rail's current entry gets, and for the same reason: a control that leads
    to the page you are already on should say so rather than be pressed. */
.topbar-icon.is-active
{
    color: var(--brand-ink);
    border-color: var(--brand);
}

.setform
{
    max-width: 560px;
}

/*  A section heading inside a form. Small and quiet: it separates, it does
    not announce - the page has one <h1> in the bar already. */
.setgroup
{
    margin: 22px 0 2px;
    font-size: 11px;
    font-weight: 650;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.setgroup:first-of-type
{
    margin-top: 4px;
}

/*  The two groups that are NOT part of the form, set apart by a rule so the
    Save button above them plainly does not belong to them. */
.setgroup-apart
{
    margin-top: 32px;
    padding-top: 20px;
    border-top: 1px solid var(--line);
}

/*  The generation's progress bar. A native <progress>, so the only thing this
    has to do is give it a size - every browser draws the rest, and a bar that
    is 150px wide by default reads as a decoration rather than as the run. */
.genbar
{
    display: block;
    width: 100%;
    max-width: 560px;
    height: 10px;
    margin: 6px 0 2px;
}

/*  ALL / NONE beside a group heading - the desktop Generate dialog's two
    buttons, in the place a heading leaves free. The heading is uppercase and
    tiny, so the buttons have to be quiet enough not to become the loudest
    thing on the screen; see .btn-quiet. */
.setgroup-tools
{
    display: inline-flex;
    gap: 6px;
    margin-left: 12px;
    vertical-align: baseline;
}

/*  A button that is a convenience, not a decision. No fill, no border until
    it is pointed at - it reads as a link that happens to be a button, which
    is exactly what All and None are. */
.btn-quiet
{
    height: auto;
    min-height: 0;
    padding: 1px 6px;
    font-size: 10px;
    font-weight: 650;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-soft);
    background: none;
    border: 1px solid transparent;
}

.btn-quiet:hover,
.btn-quiet:focus-visible
{
    color: var(--brand-ink);
    border-color: var(--line);
}

/*  Years, laid out across rather than down. Twelve one-character labels in a
    column would be a very tall list saying very little; in a row they read as
    what they are - a range with some of it turned off. */
.setform-inline
{
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
}

.check-inline
{
    min-width: 0;
    margin: 0;
}

/*  A checkbox with a label and a note under it. The note is the second line
    of the same <span>, so clicking either still ticks the box. */
.check
{
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 6px 0;
    cursor: pointer;
}

.check input
{
    margin-top: 2px;
    flex: 0 0 auto;
}

.check .sub
{
    display: block;
    font-size: 12px;
    color: var(--ink-soft);
}

/*  A label and a control on ONE line, for the page-size rows: "Rooms  [25]"
    reads as a pair, where the stacked form field reads as a question. */
.field-inline
{
    flex-direction: row;
    align-items: center;
    gap: 12px;
}

/*  ONE LINE, ALWAYS - 13 Sep 2026. Mišo: "ovakva prelamanja labela nisu
    potrebna - uvijek sve u jednu liniju ako moze".

    `min-width: 140px` is a FLOOR, not a width: a label longer than that wraps
    inside it, so "A NEW EXECUTION TYPE" became two lines and the row it
    labelled grew to match. Two lines of small uppercase beside a single input
    reads as two questions.

    `nowrap` lets the label take the width it needs and the floor keeps the
    short ones lined up with each other - which is what the floor was for. The
    control beside it is what gives way, and it has the room: these rows are a
    label and one box on a 560px form. */
.field-inline .field-label
{
    min-width: 140px;
    white-space: nowrap;
}

/*  The swatches and the three scheme buttons on the settings screen. Same
    dots as the header's pop-over - they are the same control - laid out in a
    row rather than a four-wide grid, because here there is width. */
.setrow
{
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin: 10px 0 6px;
}


/* ------------------------------------------------------------------ */
/* Programme -> year -> branch, and the way out of it                  */
/* ------------------------------------------------------------------ */

/*  Mišo, 6 Sep 2026: "neki imaju bas mnogo i nepregledne brancheve pa bi
    svuda gdje ih upotrebljavas za filter dao ipak i program->godina->branch"
    - and for the drawer, "+ search (po filtriranom) + 'x' za cisti filter".

    IT WRAPS AND EVERY CONTROL IS FULL WIDTH INSIDE THE DRAWER. The drawer is
    420px; three selects, a search box, a button and an × will not sit on one
    line at that width, and a filter whose last control is off the edge is a
    filter with a level nobody can reach. Stacked, it is five rows of a panel
    that is already a column. */
.bfilter
{
    margin: 0 0 12px;
}

.bfilter-form
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

/*  In a drawer the selects take the whole width; on a wide screen - the same
    partial on a list page - they sit side by side. `flex: 1 1 200px` does
    both without a media query and without the partial knowing where it is. */
.bfilter-sel
{
    flex: 1 1 200px;
    min-width: 0;

    /*  A programme name is "Kemijsko inženirstvo - univerzitetni študijski
        program 1. stopnje", which is 62 characters. It is allowed to be
        clipped by the control rather than to widen it: the full text is in
        the open list, which is where somebody reads it. */
    text-overflow: ellipsis;
}

/*  The year is a number and never needs the width the other two do. */
.bfilter-year
{
    flex: 0 1 120px;
}

.bfilter-q
{
    flex: 1 1 160px;
    min-width: 0;
}

/*  THE WAY OUT, and it is drawn only when there is something to clear - see
    the template. A round × in the ink colour, not in the accent: it undoes,
    and undo is not a thing to invite. */
.bfilter-clear
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex: 0 0 auto;

    border-radius: 50%;
    color: var(--ink-soft);
    text-decoration: none;

    transition: background .12s ease, color .12s ease;
}

.bfilter-clear:hover
{
    background: var(--surface-2);
    color: var(--ink);
}

.bfilter-clear:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/*  The count sits at the end of the row and takes what is left, so on a
    narrow drawer it lands on its own line rather than squeezing the search
    box to nothing. */
.bfilter .editcount
{
    margin-left: auto;
}


/* ==================================================================== */
/*  "Scheduled already" - the mark before a name                         */
/* ==================================================================== */

/*  Mišo, 6 Sep 2026: the desktop listed scheduled entities on a yellow
    background; here it should be something less invasive, "kljukica mala
    lijevo gore u uglu ispred (ne iza jer ime moze bit dugo) imena".

    A FIXED SLOT, ALWAYS. Every state renders the same 14px box - including
    the two that draw nothing - so the names form a straight column. A mark
    that took its width only when present would shift every second row and
    make the list harder to read than it was without any mark.

    NOT RED, NOT GREEN. A finished row is not "good" and an unfinished one is
    not an error; they are two states of ordinary work. So the tick is the
    reader's own accent colour, which also means the marks change with the
    theme instead of being the one blue thing left on a clay screen. */
body.admin .donemark
{
    /*  Set per element on the part state; declared here so the property
        always has a value and tools/dead-tokens.sh has something to find. */
    --frac: 0;

    display: inline-flex;
    align-items: center;
    justify-content: center;

    width: 14px;
    height: 14px;
    flex: 0 0 14px;

    /*  Nudged up a little: it sits beside a line of text and optically
        centres a touch high, which is where the eye expects a corner mark. */
    vertical-align: -1px;
    margin-right: 2px;
}

body.admin .donemark.is-full
{
    color: var(--brand);
}

/*  The part state is a dial, not a second tick: placed/total is a real
    fraction and "nearly finished" is the row worth finding. conic-gradient
    fills to --frac; the inner mask leaves a ring rather than a pie, which is
    quieter beside text. */
body.admin .donemark.is-part::before
{
    content: "";

    width: 10px;
    height: 10px;
    border-radius: 50%;

    background: conic-gradient(
        var(--brand) calc(var(--frac) * 360deg),
        color-mix(in srgb, var(--line-strong) 70%, transparent) 0
    );

    /*  A ring. -webkit-mask is still needed for Safari; the unprefixed one
        carries every other browser, and a browser that honours neither draws
        a filled dot, which is a perfectly readable fallback. */
    -webkit-mask: radial-gradient(circle, transparent 3px, #000 3.5px);
            mask: radial-gradient(circle, transparent 3px, #000 3.5px);
}

/*  'none' and 'unknown' draw nothing and hold the space. Separate rules
    rather than one, because the day somebody wants a faint outline on 'none'
    it must not also appear where the server said nothing at all. */
body.admin .donemark.is-none,
body.admin .donemark.is-unknown
{
    /* deliberately empty - the slot is the whole content */
}

/* ------------------------------------------------------------------ */
/* A subgroup row, indented under its parent                           */
/* ------------------------------------------------------------------ */

/*  THE INDENT DID NOT EXIST UNTIL 9 Sep 2026, and the whole Groups screen was
    designed around it.
    
    The design note said "subgroups are indented under their parent in one
    list, so one indented list says everything a nested one would". The
    template emitted `class="is-subgroup"` faithfully from the first day. There
    was no rule anywhere that indented it: the only selector in the product
    that mentioned the class was the opacity below, and that one is scoped to
    `.edittable` while the branch form's table is a `.listtable` - so it never
    matched either.
    
    So every subgroup read as a root. Measured across the estate
    (deploy/probe-group-shape.sh) that is 5,794 rows - 36.2% of all groups -
    and at wtt_temp, where the screen was built and looked right, it was 17 of
    681. Two and a half percent of one school is invisible; a third of the
    estate is the list not meaning what it says.
    
    --gdepth CARRIES THE LEVEL. The template sets it only past the first, so a
    plain subgroup needs no inline style and the fallback of 1 covers it.
    18px a step, against the cell's own 12px of padding: enough to read as a
    level at a glance, small enough that three levels still leave room for a
    group name in a 900px table. */
body.admin .listtable td.is-subgroup,
body.admin .edittable td.is-subgroup
{
    padding-left: calc(12px + var(--gdepth, 1) * 18px);
}

/*  In a subgroup row the name is indented; the mark goes with it rather than
    staying at the cell edge, so the ticks step in too and the hierarchy still
    reads. Both table classes, for the reason above - one of them was the only
    one this rule named, and it was the wrong one. */
body.admin .listtable td.is-subgroup .donemark,
body.admin .edittable td.is-subgroup .donemark
{
    opacity: .85;
}


/* ==================================================================== */
/*  The drawer wears the theme                                           */
/* ==================================================================== */

/*  Mišo, 6 Sep 2026: "not placed je bolje da ima onaj desni dio po temi -
    boji... i onda text unutra bijeli ili crni".

    --on-brand IS DECLARED PER THEME AND NOT COMPUTED. CSS has no portable way
    to ask "is this colour dark enough for white text" - color-contrast() is
    still not something to ship to a registrar's browser - so the answer is
    written down once per theme, where it can be checked by eye and by a
    contrast tool. All eight accents are dark enough for white today; the
    property exists so that the day somebody adds a pale one, the fix is one
    line here and not a rewrite of this block.

    THE CONTROLS DO NOT GO WITH IT. A select and a search box keep their own
    light surface inside the coloured panel: a text field drawn in accent-on-
    accent is a field nobody can read what they typed in, and "themed" was
    never meant to cost that. Same for the list rows' own text, which stays on
    the panel colour and simply flips to --on-brand. */
body.theme-teal,
body.theme-blue,
body.theme-sage,
body.theme-navy,
body.theme-violet,
body.theme-clay,
body.theme-amber,
body.theme-red,
body.theme-plum,
body.theme-slate,
body.admin
{
    --on-brand: #fff;

    /*  THE PAGE'S OWN INK, UNDER A NAME THE PANEL DOES NOT SHADOW.
    
        The drawer below re-points --ink, --ink-soft and --line at the panel's
        colours for everything inside it. Anything that has to keep the PAGE's
        colours while sitting in that panel - a select, a search box, a text
        field, all of which keep a white surface - reads these instead. Saved
        here rather than restated, so there is still exactly one place the
        light and dark values are written down. */
    --page-ink:      var(--ink);
    --page-ink-soft: var(--ink-soft);
    --page-line:     var(--line);
}

/*  THE PANEL RE-POINTS THE TOKENS, IT DOES NOT LIST THE CLASSES.
 *
 *  Mišo, 6 Sep 2026, on the detail panel: "moras malo pazit na boju text na
 *  backgrounds od teme.. nije samo ovdje".
 *
 *  He was right, and the reason is the shape this block used to have: it
 *  named the classes one by one - drawer-title, todo-course, todo-type,
 *  todo-plan and the rest - and made each of them white. That works for
 *  exactly the classes somebody remembered on the day, and every element
 *  added afterwards keeps --ink-soft, which is a quiet grey designed for
 *  white paper. On the accent panel it is very nearly the panel itself. The
 *  detail drawer was built after this list and inherited none of it: its
 *  labels, its execution type, its guid and its footnote were all drawn in
 *  grey on green.
 *
 *  A list of class names cannot be right for longer than it takes to write
 *  the next screen. So the panel now redefines the TOKENS. Everything inside
 *  it that says var(--ink) or var(--ink-soft) - today's classes and every
 *  future one - gets the panel's own ink without anybody adding a line here.
 *
 *  The RELATIONSHIP is preserved, not the value: primary stays full strength,
 *  secondary stays quieter than primary, hairlines stay hairlines. 78% of the
 *  panel's ink reads as secondary at every one of the eight accents.
 */
body.admin .drawer
{
    --ink:         var(--on-brand);
    --ink-soft:    color-mix(in srgb, var(--on-brand) 78%, transparent);
    --line:        color-mix(in srgb, var(--on-brand) 22%, transparent);
    --line-strong: color-mix(in srgb, var(--on-brand) 38%, transparent);
    --surface-2:   color-mix(in srgb, var(--on-brand) 12%, transparent);

    /*  --brand-ink is a DARKER SHADE OF THE ACCENT, meant for accent-coloured
        text on white paper. On the accent itself it is the same colour twice.
        Inside the panel it has to be the panel's ink. */
    --brand-ink:   var(--on-brand);

    background: var(--brand);
    color: var(--on-brand);
    border-left-color: color-mix(in srgb, #000 22%, var(--brand));
}

/*  Separators: the panel's own ink at low alpha, never a fixed grey - a grey
    hairline over clay looks like dirt and over navy disappears. Kept as an
    explicit rule because these two set border-color from a literal rather
    than from --line. */
body.admin .drawer .todo-item,
body.admin .drawer .drawer-foot
{
    border-color: color-mix(in srgb, var(--on-brand) 22%, transparent);
}

body.admin .drawer .drawer-shut
{
    color: var(--on-brand);
}

body.admin .drawer .drawer-shut:hover
{
    background: color-mix(in srgb, var(--on-brand) 16%, transparent);
    color: var(--on-brand);
}

/*  The bin follows the close button onto the accent panel for the same reason
    it did: --ink-soft is invisible on brand. Its hover keeps the red, but as
    a PLATE under an on-brand glyph rather than red ink - red on teal is muddy
    at 14 pixels, and the plate is what carries the meaning anyway. */
body.admin .drawer .drawer-bin
{
    color: var(--on-brand);
}

body.admin .drawer .drawer-bin:hover,
body.admin .drawer .drawer-bin:focus-visible
{
    background: color-mix(in srgb, var(--danger, #c0392b) 55%, transparent);
    color: var(--on-brand);
}

/*  The count badge was accent-on-accent the moment the panel became accent -
    invisible, which is the same bug reported once already on the "Not placed"
    button. On the panel it becomes a plate of the panel's own ink. */
body.admin .drawer .editcount
{
    color: var(--on-brand);
    background: color-mix(in srgb, var(--on-brand) 20%, transparent);
}

/*  The controls keep a real surface, so they keep the PAGE's ink - what is
    behind them is white paper again, and accent-on-accent is a field nobody
    can read what they typed in.

    --page-ink and NOT --ink: the panel above re-points --ink at its own white,
    and that white is exactly what a white field must not use. The saved token
    is why the token switch above can be blanket and still leave the controls
    alone - and it is set on body, which the panel does not shadow. */
body.admin .drawer .input,
body.admin .drawer select,
body.admin .drawer input[type="search"],
body.admin .drawer input[type="text"]
{
    background: var(--surface);
    color: var(--page-ink);
    border-color: color-mix(in srgb, #000 18%, var(--brand));
}

/*  And anything inside a control that was deliberately quieter - an option
    group's hint, a placeholder - keeps the page's quiet grey rather than the
    panel's. */
body.admin .drawer .input::placeholder,
body.admin .drawer input[type="search"]::placeholder,
body.admin .drawer input[type="text"]::placeholder
{
    color: var(--page-ink-soft);
}

body.admin .drawer .bfilter-clear
{
    color: color-mix(in srgb, var(--on-brand) 80%, transparent);
}

body.admin .drawer .bfilter-clear:hover
{
    background: color-mix(in srgb, var(--on-brand) 16%, transparent);
    color: var(--on-brand);
}

/*  The submit button: the panel's ink as an outline, so it reads as a control
    without introducing a third colour. */
body.admin .drawer .btn
{
    background: color-mix(in srgb, var(--on-brand) 14%, transparent);
    border-color: color-mix(in srgb, var(--on-brand) 38%, transparent);
    color: var(--on-brand);
}

body.admin .drawer .btn:hover
{
    background: color-mix(in srgb, var(--on-brand) 24%, transparent);
}

/*  THE DELETE BUTTON, IN THE DRAWER - 13 Sep 2026.

    `.btn-danger` is red ink on a transparent plate, which is right on a white
    settings screen and unreadable on the teal panel: red text on teal is the
    lowest-contrast pair either colour makes. So on the panel it inverts - a
    red PLATE with the panel's own ink on it - which also puts it the right way
    round against the Cancel beside it. The outline/filled argument in
    `.btn-danger`'s own note still holds: there, the filled button was the
    ordinary one; here, everything else in the drawer is an outline, so filled
    is what marks this one out. */
body.admin .drawer .btn-danger
{
    background: color-mix(in srgb, var(--danger) 70%, transparent);
    border-color: color-mix(in srgb, var(--danger) 82%, transparent);
    color: var(--on-brand);
}

body.admin .drawer .btn-danger:hover
{
    background: var(--danger);
}

/* ==================================================================== */
/*  The drawer's filter, in two lines                                    */
/* ==================================================================== */

/*  Mišo: "filteri desno za not placed neka budu optimizirani za 2 linije".
    It was three, sometimes four.

    THE ARITHMETIC, for a 420px drawer with 16px of padding a side - 388px of
    content, 8px gaps:

        line 1   programme 210 + year 96 + gap 8            = 314, grows to fit
        line 2   branch 150 + word 110 + go 34 + clear 26   = 344 with gaps

    Basis values, not widths: flex still shares out what is left, so a wider
    drawer or a narrower one moves the same two rows rather than reflowing to
    three. The one thing that must not stretch is the year, which is a number.

    ONLY IN THE DRAWER. The same partial sits on the courses list, where the
    row is a page wide and one line is right - so these rules are scoped, and
    the unscoped ones above stay as they were. */
body.admin .drawer .bfilter-form
{
    gap: 8px;
}

body.admin .drawer .bfilter-prog
{
    flex: 1 1 210px;
}

body.admin .drawer .bfilter-year
{
    flex: 0 1 96px;
}

body.admin .drawer .bfilter-branch
{
    flex: 1 1 150px;
}

body.admin .drawer .bfilter-q
{
    flex: 1 1 110px;
}

/*  THE SUBMIT IS AN ICON NOW - see the note in _branchpick.php. Square and
    the same size as the × beside it, so the two read as a pair rather than as
    a button and an afterthought. `flex: 0 0 auto` because a button that grew
    would eat the width the search box needs. */
.bfilter-go
{
    flex: 0 0 auto;

    display: inline-flex;
    align-items: center;
    justify-content: center;

    width: 34px;
    padding: 0;
}


/* ==================================================================== */
/*  "Where could this one go?"                                           */
/* ==================================================================== */

/*  Mišo, 6 Sep 2026: "sa klikom na nealocirano pokazemo na lijevoj strani
    gdje moze bez konflikta - obojim mjesto sa drugom bojom i unutra napisem
    'Click here to place'".

    A DIFFERENT COLOUR, AND NOT THE ACCENT. The accent is already the reader's
    theme and is on the drawer, the current rail item, the today mark and the
    line under the topbar; one more accent rectangle would be lost among them.
    A free hour is a suggestion about the future, so it is drawn as a dashed
    outline over a wash rather than as a solid block - it must not read as a
    lesson that is already there, which is the one misreading that would cost
    somebody real work.

    z-index BELOW THE CARDS. These are hours that are empty FOR THIS LESSON,
    but the column belongs to one room or one lecturer and other people's
    lessons are in it. Painting over those would hide exactly what the reader
    needs to see beside the offer. */
body.admin .slotfree
{
    position: absolute;
    left: 2px;
    right: 2px;
    z-index: 1;

    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;

    padding: 2px 6px;
    overflow: hidden;

    border: 1px dashed color-mix(in srgb, var(--brand) 55%, transparent);
    border-radius: var(--radius-s);
    background: color-mix(in srgb, var(--brand) 10%, transparent);

    font-size: 11px;
    line-height: 1.25;
    pointer-events: none;
}

body.admin .slotfree-word
{
    font-weight: 650;
    color: var(--brand-ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

body.admin .slotfree-rooms
{
    color: var(--ink-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*  FREE FOR THE PEOPLE, BUT NO ROOM BIG ENOUGH IS EMPTY. Still offered - it
    is a real answer and hiding it would silently drop the hours where only
    the room is short - but drawn without the wash, so the eye goes to the
    ones that are free all the way through. */
body.admin .slotfree-noroom
{
    background: none;
    border-style: dotted;
    opacity: .72;
}

/*  A 30-minute block has room for one line and not two. */
body.admin .slotfree
{
    container-type: size;
}

@container (max-height: 34px)
{
    body.admin .slotfree-rooms
    {
        display: none;
    }
}

/* -------------------------------------------------------------------- */
/*  The drawer's half: the picked row, and what the colour means          */
/* -------------------------------------------------------------------- */

/*  The whole row is the hit area. Inheriting colour and decoration so that
    turning the row into a link changed nothing about how it reads - a list of
    four hundred blue underlined rows would be unreadable, and the pointer
    plus the hover plate say "clickable" perfectly well. */
body.admin .todo-hit
{
    display: block;
    color: inherit;
    text-decoration: none;
}

body.admin .todo-item:hover
{
    background: color-mix(in srgb, var(--on-brand, var(--ink)) 10%, transparent);
}

body.admin .todo-hit:focus-visible
{
    outline: 2px solid var(--on-brand, var(--brand));
    outline-offset: -2px;
    border-radius: var(--radius-s);
}

/*  THE PICKED ONE. A left bar rather than a fill: the drawer is already the
    accent colour, so a fill would have to be a second colour on top of it,
    and a bar in the panel's own ink is unambiguous at every theme. */
body.admin .todo-picked
{
    background: color-mix(in srgb, var(--on-brand, var(--ink)) 14%, transparent);
    box-shadow: inset 3px 0 0 var(--on-brand, var(--brand));
}

body.admin .placenote
{
    margin: 0 0 12px;
    padding: 10px 12px;
    border-radius: var(--radius-m);
    background: color-mix(in srgb, var(--on-brand, var(--ink)) 12%, transparent);
}

body.admin .placenote-what
{
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin: 0 0 6px;
    font-size: 13px;
}

body.admin .placenote-clear
{
    margin-left: auto;
    color: inherit;
    font-size: 12px;
    text-decoration: underline;
    white-space: nowrap;
}

/*  The "not checked" line is the one somebody has to read, so it is the one
    line in the panel that is not quieter than the rest. */
body.admin .placenote-warn
{
    font-weight: 600;
    color: var(--on-brand, var(--ink)) !important;
}

/*  THE API'S OWN SENTENCE, when the lookup failed - 10 Sep 2026.

    Quieter than the notice it sits under and in a monospaced face, because it
    is a diagnostic and not a sentence for a referat: it should read as
    something copied out of a log, so that a person who does not need it can
    skip it and a person who does can quote it. Wrapping is allowed - an
    exception message is not a label, and truncating it on screen would hide
    exactly the end of it, where the cause usually is. */
body.admin .placenote-why
{
    display: block;
    margin: 3px 0 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11px;
    font-weight: 400;
    line-height: 1.35;
    opacity: .75;
    overflow-wrap: anywhere;
}

/*  ROOMS THAT ARE FREE AND OUT OF REACH - 19 Sep 2026, patch309/patch310.

    NOT A WARNING, and deliberately not dressed as one. Nothing has gone
    wrong: the rooms are simply in another building, which is a fact about the
    school's geography rather than about this placement. So it sits at the
    same weight as the rest of the panel and takes only an italic - enough to
    read as an aside about the list above it, not enough to compete with the
    "not checked" line, which IS the one somebody has to read.

    The room names wrap. Three of them at a school that spells rooms
    "Predavalnica P-17 [FKKT-1.04]" is longer than any drawer is wide, and
    truncating would cut off exactly the code that tells two of them apart. */
body.admin .placenote-far
{
    font-style: italic;
    opacity: .85;
    overflow-wrap: anywhere;
}

body.admin .placenote .drawer-note
{
    margin-bottom: 4px;
}

body.admin .placenote .drawer-note:last-child
{
    margin-bottom: 0;
}

/*  THE SAME BLOCK, OUT ON THE PAGE - 8 Sep 2026.

    Everything above is written for the drawer, where `--on-brand` is the ink
    on an accent panel. A move draws this block ABOVE THE GRID with the drawer
    shut, on the page's own background - where `--on-brand` is either unset or,
    worse, still the drawer's, and the whole note would be one shade off from
    invisible.

    So the move copy states its own colours and does not inherit that variable
    at all. It is not a variant of the drawer's look; it is the same content in
    a different room. */
body.admin .placenote-move
{
    margin: 0 0 10px;
    border: 1px solid var(--brand);
    background: color-mix(in srgb, var(--brand) 10%, var(--surface, transparent));
    color: var(--ink);
}

body.admin .placenote-move .placenote-warn
{
    color: var(--ink) !important;
}

body.admin .placenote-move .placenote-clear
{
    color: var(--brand);
}

/*  WHICH OF THE TWO THINGS IS HAPPENING, read before the subject's name.
    Moving and placing write differently - one updates a row, the other adds
    one - so the word carries its weight and gets the emphasis a chip gets. */
body.admin .placenote-verb
{
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: color-mix(in srgb, currentColor 18%, transparent);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/*  The panel's one action, given the room a button needs under a list of
    definition pairs that are all quiet. */
body.admin .detail-do
{
    margin: 12px 0 0;
}


/* ==================================================================== */
/*  A plus on every hour                                                 */
/* ==================================================================== */

/*  Mišo, 8 Sep 2026: "ta dva plusića ... stavio bih svuda".

    A NARROW STRIP AT THE RIGHT EDGE OF EACH HOUR, invisible until that hour
    is hovered or tabbed into. It sits ABOVE the cards, because an occupied
    hour is one of the hours this offers - both writes can overlap on purpose -
    but it is three characters wide, so a card's own name and its click target
    are untouched.

    z-index rather than document order alone: the cards are positioned, so a
    later sibling without one would still lose to a card that has one. */
body.admin .slotadd
{
    position: absolute;
    right: 2px;
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    height: var(--slot);
    padding-right: 2px;

    /*  TWO ICONS SINCE 8 Sep 2026 - a lecture and a booking - so they need a
        hair of space between them. 3px and not more: the strip has to stay
        narrow enough that a card's own name and click target are untouched. */
    gap: 3px;

    /*  NOT `display: none` - a hidden control cannot be tabbed to, and this
        screen is used on machines where the keyboard is how work gets done.
        Transparent and still in the layout keeps it reachable. */
    opacity: 0;
    transition: opacity 90ms ease;
}

/*  ONE HOUR AT A TIME, AND THE STRIP IS ITS OWN TRIGGER.
 *
 *  A COLUMN-WIDE REVEAL WAS TRIED ON 9 Sep 2026 AND REVERTED THE SAME HOUR.
 *
 *  The reasoning was that these icons are hard to discover: they are three
 *  characters wide and transparent, so the pointer has to find something it
 *  cannot see. `.grid-day:hover .slotadd { opacity: .35 }` was added to ghost
 *  the whole column in on entering a day.
 *
 *  It is worse, and the screenshot settled it in one look: fourteen hours of
 *  a day is fourteen PAIRS of icons, a solid vertical ladder of them down the
 *  full height of the column, over and past the cards. Mišo: "sad pokazujes
 *  cijelu vertikalu bez potrebe... prije ovog zadnjeg deploya mi je bilo
 *  bolje - 2 ikonice su se pojavile na desnoj strani i onda sam se odlucio
 *  ako cu i koju kliknuti... to je sve".
 *
 *  THE DIAGNOSIS WAS WRONG, not just the remedy. "I cannot find them" was
 *  read as a structural fault in the target; it was a one-time fact about
 *  somebody who had not yet been told where they are. Once seen, the narrow
 *  per-hour reveal is exactly right: it answers ONE hour, the hour under the
 *  pointer, and asks nothing of the rest of the week. A control that reveals
 *  its thirteen neighbours to be found is not more discoverable, it is
 *  louder.
 *
 *  So: the strip reveals itself, and nothing else does. If discovery ever
 *  needs solving again, solve it once - a line in the help, or a first-run
 *  hint - and not by making every hour of every day speak up. */
body.admin .slotadd:hover,
body.admin .slotadd:focus-within
{
    opacity: 1;
}

@media (prefers-reduced-motion: reduce)
{
    body.admin .slotadd
    {
        transition: none;
    }
}

/*  THE ICON MAY NOT BE TALLER THAN THE HOUR IT BELONGS TO - 13 Sep 2026.

    Mišo: "ove male ikonice mi izlaze za dva pixela gore iznad okvira".

    MEASURED, and the arithmetic is the whole bug. schedule.css:66 sets
    `--slot: 15px` - the half-hour, and the comment there says everything else
    is a multiple of it. `.slotadd` is `height: var(--slot)` with
    `align-items: center`, and this button was a flat **18px**. An 18px item
    centred in a 15px box hangs one and a half pixels out of each end, which
    is the two pixels he is seeing above the line - and it was doing the same
    below, where there is no line for the eye to catch it against.

    `min()` AND NOT A SMALLER NUMBER. Hard-coding 15 would fix this school and
    break the next one: --slot is a variable because a school with a longer
    teaching hour gets a taller row, and there the button should stay 18 rather
    than shrink to match a constant. `min(18px, var(--slot))` is "as big as it
    was, but never taller than the row it sits in", which is the rule rather
    than today's value.

    THE WIDTH IS LEFT AT 18. Nothing constrains the strip horizontally - it is
    positioned at the right edge and is as wide as it needs - so narrowing it
    would give up click target for no reason at all. */
body.admin .slotadd-btn
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: min(18px, var(--slot, 18px));
    border: 1px solid var(--brand);
    border-radius: 4px;
    background: var(--surface, #fff);
    color: var(--brand);
    line-height: 1;
}

body.admin .slotadd-btn:hover
{
    background: var(--brand);
    color: var(--on-brand, #fff);
}

body.admin .slotadd-btn:focus-visible
{
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

/*  THE BOOKING ONE IS NOT THE BRAND COLOUR, and that is the whole point of
    drawing two icons rather than two pluses. Mišo asked for "ikone koje
    nazorno pokazuju rezervaciju ili predavanje" - the shapes differ (a card,
    a key) and so does the colour, so the two are told apart at a glance on a
    control that is eighteen pixels wide and repeated once per hour.

    --accent where the theme has one, falling back to a warm tone rather than
    to the brand: falling back to the brand would make the two identical at
    exactly the schools whose theme is thinnest. */
body.admin .slotadd-book
{
    border-color: var(--accent, #a2601f);
    color: var(--accent, #a2601f);
}

body.admin .slotadd-book:hover
{
    background: var(--accent, #a2601f);
    color: var(--on-brand, #fff);
}

body.admin .slotadd-book:focus-visible
{
    outline-color: var(--accent, #a2601f);
}


/* ==================================================================== */
/*  The booking composer - the drawer the key icon opens                 */
/* ==================================================================== */

/*  IT SHARES THE DRAWER WITH THE OUTSTANDING LIST, so it inherits .drawer
    entirely and only says what is different: this one is a form, and a form
    in a 420px rail needs its rows to stack rather than to sit side by side.

    NO PADDING OF ITS OWN. `.drawer` already carries 14px/16px/18px and
    `overflow-y: auto` for the whole rail; a second padding here would inset
    the form from the header above it by exactly one gutter, which reads as a
    misalignment rather than as a margin. */
body.admin .drawer-book .drawer-body
{
    min-height: 0;
}

body.admin .bookrail
{
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/*  WHERE, IN WORDS, BEFORE ANYTHING ELSE. The composer is reached by pressing
    an eighteen-pixel icon on one of three hundred hours; the first thing it
    owes the reader is which one was pressed. */
body.admin .bookrail-where
{
    margin: 0;
    font-size: 14px;
}

body.admin .bookrail-week
{
    margin-left: 8px;
    color: var(--ink-soft);
    font-size: 12px;
}

/*  THE RECTANGLE - hours x days x weeks, the same three numbers and the same
    order as the availability bar, because the two screens write one table. */
body.admin .bookrail-size
{
    display: flex;
    gap: 8px;
}

body.admin .bookrail-size label
{
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 3px;
    color: var(--ink-soft);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

body.admin .bookrail-num
{
    width: 100%;
}

body.admin .bookrail-field
{
    display: flex;
    flex-direction: column;
    gap: 3px;
}

body.admin .bookrail-label
{
    color: var(--ink-soft);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

/*  A FOLD PER LIST. Rooms, lecturers, groups and the subject pair are four
    lists of up to a few hundred rows each, and all four open at once is a
    rail nobody can reach the Book button in. <details> and not a script: the
    browser has had this control for years and it survives with JavaScript
    off, which is this application's rule. */
body.admin .bookrail-sec
{
    border: 1px solid var(--line, #e3e3e3);
    border-radius: 6px;
    padding: 6px 8px;
}

body.admin .bookrail-sec > summary
{
    cursor: pointer;
    font-size: 12px;
    font-weight: 600;
    list-style-position: inside;
}

/*  THE SCROLLING LIST ITSELF. A fixed height rather than a growing one: an
    open fold over 263 lecturers would push everything below it - including
    the Book button - past the bottom of a rail that does not scroll as one
    piece. */
body.admin .bookpick
{
    max-height: 190px;
    overflow-y: auto;
    margin-top: 6px;
    border-top: 1px solid var(--line, #e3e3e3);
    padding-top: 6px;
}

body.admin .bookpick-row
{
    display: flex;
    align-items: baseline;
    gap: 6px;
    padding: 2px 0;
    font-size: 13px;
    line-height: 1.35;
    cursor: pointer;
}

body.admin .bookpick-row:hover
{
    background: color-mix(in srgb, currentColor 6%, transparent);
}

body.admin .bookrail-empty
{
    margin: 6px 0 0;
}

/*  THE CASCADE. Three selects and the narrow button, wrapping rather than
    scrolling sideways - in a 420px rail with a Slovenian programme name in
    the first one, one line is not a promise that can be kept. */
body.admin .bookrail-cascade
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

body.admin .bookrail-cascade select
{
    flex: 1 1 45%;
    min-width: 0;
}

/*  THE WRITE, AND THE WAY OUT, on one line at the foot of the form. */
body.admin .bookrail-do
{
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 2px;
}

/*  THE ROW THAT WILL PLACE RATHER THAN CHOOSE. Same list, one different verb -
    so a mark rather than a different layout: the reader is looking at the same
    rows they were, and the difference is what pressing one does. */
body.admin .todo-at .todo-hit
{
    width: 100%;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

/*  THE CLASH, ON THE ROW IT BELONGS TO. Not an error colour: overlapping is a
    normal thing to do on purpose, and painting the question red would tell
    somebody they had done something wrong before they had done anything. */
body.admin .todo-clash
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin: 6px 0 2px;
    padding: 8px 10px;
    border-radius: var(--radius-m);
    background: color-mix(in srgb, var(--on-brand, var(--ink)) 16%, transparent);
}

body.admin .todo-clash-why
{
    flex: 1 1 100%;
    margin: 0;
    font-size: 12px;
}


/* ==================================================================== */
/*  The plan, as lines                                                   */
/* ==================================================================== */

/*  Mišo, 6 Sep 2026: "otvorio si course i imao odmah turn linije za edit! i
    tamo si upisao duration... A ovdje - kao da je interfejs glavni a ne kupac
    - editiramo ko u db."

    A TABLE OF CONTROLS, NOT A STACK OF FORMS. The row has to stay a row: the
    eye reads down the Hours column comparing one turn against the next, and
    that only works if the boxes line up and none of them is wider than what
    it holds. So every control is sized to its content - a week is three
    digits, a plan is about twenty characters - rather than stretched to the
    cell. */
body.admin .planlines td
{
    /*  Tighter than the reading tables: this one is worked in, not read, and
        a row of inputs at the normal row height is a lot of empty space. */
    padding-top: 4px;
    padding-bottom: 4px;
    vertical-align: middle;
}

body.admin .planlines .input
{
    height: 30px;
    padding: 0 8px;
}

/*  "1-16:2+1". Monospace, because it is a written form with a grammar and the
    digits have to line up when the reader scans the column - the same reason
    the outstanding drawer sets the plan in it. Wide enough for the shapes
    that are 98.5% of the estate; the long ones scroll inside the box rather
    than widening the table. */
body.admin .planbox
{
    width: 14ch;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
}

/*  THE WEEK NUMBER, AND THE SPINNER THAT WAS SITTING ON IT - 10 Sep 2026.

    Mišo, of "valid to week": "a turn weeks vidim  malo pokriven". He was
    right, and the cause is the combination rather than either half: a
    type=number draws its own up/down arrows INSIDE the box, and
    `text-align: right` pushes the digits under exactly that corner. At 6ch a
    two-digit week - 16 - lost half its second digit behind the arrows.
    Nothing was broken; it was unreadable, which for a number a planner is
    checking is the same thing.
    
    8ch rather than hiding the spinner. The arrows are worth keeping - a week
    is stepped far more often than it is typed - and this column holds at most
    two digits, so two characters of room is the whole fix. The field still
    reads right-aligned against its neighbour.  */
body.admin .weekbox
{
    width: 8ch;
    text-align: right;
    padding-right: 2px;
}

body.admin .roombox
{
    max-width: 16ch;
}

/*  A plan the API will not recompose - a "#" or "=" tail, or one that does
    not parse. Shown exactly as the desktop wrote it and NOT in a box: an
    editable-looking field that refuses every save is worse than a plain
    label that says why. 283 turns estate-wide. */
body.admin .planlocked
{
    display: inline-block;

    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    color: var(--ink-soft);

    padding: 0 6px;
    border-bottom: 1px dashed var(--line-strong);
    cursor: help;
}

/*  Who teaches it. The cell itself only bounds the width - the control inside
    it is the next rule. */
body.admin .linewho
{
    max-width: 24ch;
    font-size: 12px;
}

/*  A MULTIPLE SELECT THAT BEHAVES LIKE A TABLE CELL - 6 Sep 2026.

    Mišo: "ovdje je jako bitno da mijenjam direktno i lecturers - a moze bit
    jedan ili vise njih zajedno". The column was clipped read-only text and
    changing it meant opening the turn's own page: the walk the lines exist to
    remove.

    `size="1"` in the markup and a height here. A <select multiple> with no
    size renders as a full-height list box, which in a table row pushes every
    other cell to the top of a box four times too tall. Three rows is enough
    to show that more than one name can be chosen - the thing a reader has to
    notice before they think to hold Ctrl - without any row growing taller
    than the buttons beside it.

    It SCROLLS rather than growing: six lecturers on one line is real, and a
    row that grew with them would make the whole table ragged. */
body.admin .linewho .whobox
{
    width: 100%;
    min-width: 12ch;
    height: 4.6em;
    padding: 2px 4px;
    font-size: 12px;
    overflow-y: auto;
}

/*  The chosen ones read as chosen with the control unfocused. Several
    browsers grey the highlight out on blur, and on a table of twelve lines
    that makes it impossible to see who is on which. */
body.admin .linewho .whobox option:checked
{
    background: color-mix(in srgb, var(--brand) 18%, transparent);
    color: var(--ink);
    font-weight: 600;
}

body.admin .lineact
{
    text-align: right;
    white-space: nowrap;
}

/*  ...EXCEPT ON AN OPEN LINE, WHERE THE CELL HOLDS WORDS - 15 Sep 2026.
    Mišo, the fourth report about this row: "ovaj cancel mi bjezi".

    THREE THINGS HAD TO BE TRUE AT ONCE and each is right on its own.

      1. `table.planlines` is `table-layout: fixed`, so a cell never grows to
         fit what is in it. That was deliberate and is worth keeping: four
         execution types draw four tables, and without it their columns agree
         about nothing.
      2. `th:nth-child(6)` is 118px, and its comment says what the number was
         measured against - "THREE ICONS AND THE GAPS BETWEEN THEM".
      3. `white-space: nowrap` above, so the cell cannot break.

    But the SHUT row holds three icons and the OPEN row holds `Save`, the
    duplicate icon and `Cancel` - two worded buttons. 118px was measured on one
    state and applied to both, and in Slovenian and Croatian the words are
    longer still: Shrani / Prekliči, Spremi / Odustani. The cell cannot grow,
    cannot wrap, and the overflow is clipped by the drawer's right edge - so
    Cancel is there, and half of it is off-screen.

    WRAPPING RATHER THAN A WIDER COLUMN, and that is the choice. Widening
    column six would take the width off LECTURERS on every table on the screen,
    shut rows included, to fix a cell that is only ever wrong on the ONE line
    that is open. A wrapped cell costs a second line on that one row and
    nothing anywhere else.

    The nowrap above is left exactly as it is: on a shut row three icons on one
    line is what it is for.

    AND THERE ARE FOUR SUCH CELLS, NOT ONE. courseform.php draws `td.lineact`
    in four rows and only the first is the three-icon one:

        line 1652  .line-shut                three icons        - nowrap, right
        line 1984  .line-open/.line-refused  Save, icon, Cancel - the screenshot
        line 2165  .line-new                 turnLineNew, Cancel
        line 2239  .line-new                 turnLineNew, Cancel

    `turnLineNew` is a phrase, not a word, so the two `.line-new` rows are
    worse off than the one that was reported and nobody had said so yet.
    Scoping this by hand to the row that was complained about would have left
    them clipped. */
body.admin .planlines .line-open > td.lineact,
body.admin .planlines .line-refused > td.lineact,
body.admin .planlines .line-new > td.lineact
{
    white-space: normal;

    /*  Wrapped inline-block buttons would otherwise sit on each other. */
    line-height: 2.2;
}

/*  THE ROW THAT CAME BACK REFUSED. Marked so the reader can see WHICH line
    the message at the top of the screen is about - a refusal that named no
    row would send somebody checking all of them. */
body.admin .line-refused > td
{
    background: color-mix(in srgb, var(--brand) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--brand);
}

/*  The add row, set apart from the saved ones without a second colour: it is
    the same table and the same shape, one step quieter until it is used. */
body.admin .line-new > td
{
    border-top: 2px solid var(--line-strong);
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/*  The forms the rows point at carry only hidden fields, so they must take no
    space at all - without `display:none`, which would stop some browsers
    submitting them. */
body.admin .lineform
{
    margin: 0;
    padding: 0;
    border: 0;
    height: 0;
    overflow: hidden;
}


/* ==================================================================== */
/*  A section per execution type                                         */
/* ==================================================================== */

/*  Mišo, 6 Sep 2026: "sad kad je web - napravis SEKCIJE jednu ispod druge a
    svaka je svoj tip izvodenja".

    The desktop shows one type at a time behind a picker because a dialog has
    one page. A web page does not, so all of them are open at once and the
    reader compares lectures against labs without changing anything. What that
    costs is vertical space, and what pays for it is that each section is
    SHORT - two or three lines - so the whole plan is usually one screen. */
/*  18px between sections became 12px and then 4px on 11 Sep, with the heading
    itself cut to about two thirds of its height - Mišo: "samo pruge sa titlom
    načina izvođenja mozes napravit uže - da je visina manje, jos vise
    pokazujuci više stvari dole", then "prazan prostor između execution type
    sekcija bi skroz smanjio". The sections are the frame, not the content;
    every pixel they give back is a turn line the reader can see without
    scrolling. What still separates them is the accent rule under each
    heading, which is a stronger boundary than any amount of air. */
body.admin .planpart
{
    margin: 0 0 4px;
}

/*  THE COLUMN HEADS ARE READ ONCE. `body.admin .listtable th` gives every
    heading row 9px top and bottom, which is right for a list somebody scrolls
    and wrong for a section that is two rows tall - on this screen the stripe
    saying HOURS / WEEKS was taller than the line underneath it.

    Mišo, 11 Sep: "posto bi jos samo malo kompaktno napravio prazan prostor
    između execution type sekcija bi skroz smanjio". Three sections on a course
    is three of these stripes and three gaps; halving both is a whole turn line
    given back per section. */
body.admin .planlines thead th
{
    padding-top: 4px;
    padding-bottom: 4px;
}

/*  And no rule under the last line of a section: the section below it starts
    with its own accent rule two pixels later, and two horizontal lines that
    close to each other read as a mistake rather than as a boundary. */
body.admin .planlines tbody tr:last-child > td
{
    border-bottom: 0;
}

body.admin .planpart:last-of-type
{
    margin-bottom: 0;
}

/*  The heading carries the type, the chief teacher, the way through to what
    belongs to the type rather than to a turn, and - since 11 Sep - the `+`
    that adds a line to this section. One row, so the four read as a sentence
    with a control at the end rather than as four separate things. */
body.admin .planpart-head
{
    display: flex;
    gap: 10px;

    /*  ONE ROW, ALWAYS - 11 Sep 2026. Mišo: "naslovnu glavu za tip izvođenja
        bi napravio da zauzme samo jedan red".

        It wrapped, and what wrapped was worth nothing: a long type name, or a
        chief teacher with three given names, pushed the "Execution types…"
        link onto a second line and the section heading became two. With four
        sections on a course that is four wasted lines above a table whose
        whole argument is that a line is one line.

        Nothing is dropped to get there - the two labels truncate and the link
        keeps its width, because the link is the only part that is a control.  */
    flex-wrap: nowrap;
    white-space: nowrap;

    /*  Tightened 11 Sep. `align-items: center` rather than baseline, because
        the row now holds a control - the `+` - and a button aligned on the
        text's baseline sits visibly low. */
    align-items: center;

    margin: 0 0 3px;
    padding-bottom: 2px;

    font-size: 13px;
    line-height: 1.5;
    border-bottom: 2px solid var(--brand);
}

/*  The type takes what it needs and gives the rest back. `min-width: 0` is
    what lets a flex item shrink below its text at all - without it the
    ellipsis never appears and the row overflows instead. */
body.admin .planpart-type
{
    flex: 0 1 auto;
    min-width: 0;

    overflow: hidden;
    text-overflow: ellipsis;

    font-weight: 650;
    color: var(--ink);
}

body.admin .planpart-chief
{
    flex: 0 1 auto;
    min-width: 0;

    overflow: hidden;
    text-overflow: ellipsis;

    font-weight: 400;
    font-size: 12px;
    color: var(--ink-soft);
}

/*  Pushed to the far end: it leaves this table, and a link that leaves should
    not sit where the eye lands first. */
body.admin .planpart-more
{
    flex: 0 0 auto;
    margin-left: auto;
    padding-left: 10px;
    font-weight: 400;
    font-size: 12px;
}

/*  ADD A LINE OF THIS TYPE. The last thing in the heading and the only button
    in it - "skroz na desno", where the eye arrives having read the name of the
    type it is about to add to.

    Quiet until aimed at, like the pencil on a shut row: this is a section
    heading, not a toolbar, and a filled accent button here would outweigh the
    twenty lines underneath it. */
body.admin .planpart-plus
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;

    width: 22px;
    height: 22px;
    margin-left: 2px;

    color: var(--ink-soft);
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    text-decoration: none;
}

body.admin .planpart-plus:hover,
body.admin .planpart-plus:focus-visible
{
    color: var(--brand);
    border-color: var(--brand);
    background: color-mix(in srgb, var(--brand) 8%, transparent);
    text-decoration: none;
}

/*  A section whose execution type is gone - orphaned turns - has neither the
    link nor the `+`, because there is nothing to open and nothing to add to.
    Its heading is then just the type name at the left, which is right: it is
    a statement about the rows below it, not an offer.

    THE LAST SECTION IS THE ONE THAT ADDS A TYPE, and it is quieter than the
    real ones: it is an offer, not a part of the plan. A full accent rule
    under it would give the same weight to "Lectures" and to "Another
    execution type". */
body.admin .planpart-add .planpart-head
{
    border-bottom-color: var(--line);
    color: var(--ink-soft);
    font-weight: 500;
}

/*  Every section repeats the column headings, and that is deliberate: three
    tables under one header row would leave the third one's columns unlabelled
    by the time the reader has scrolled to it. On the ADD section there is
    nothing to label - it is one row - so the head is left out of the markup
    entirely rather than hidden here. */

/*  The row actions: Save, then two icon buttons. Kept together and to the
    right, so the eye running down the Hours column is never crossing them. */
body.admin .lineicon
{
    display: inline-flex;
    align-items: center;
    justify-content: center;

    width: 30px;
    padding: 0;
    margin-left: 4px;

    color: var(--ink-soft);
}

body.admin .lineicon:hover
{
    color: var(--ink);
}

/*  Delete is a colour only on hover. A row with a permanently red control in
    it reads as a warning about the row, which it is not - and this one is
    pressed by accident far less often when it is quiet until aimed at. */
body.admin .lineicon-danger:hover
{
    color: var(--danger, #c0392b);
    border-color: var(--danger, #c0392b);
}


/* ==================================================================== */
/*  A free hour you can press                                            */
/* ==================================================================== */

/*  THE RECTANGLE WALKS WITH THE CURSOR - 6 Sep 2026.

    Mišo: "kad stavljam schedule mi pokaze cijelo jedno podrucje i ja stavim na
    donji kraj podrucja (kliknem) - a on mi postavi ustvari na vrh jedan
    satic... najbolje bi bilo da ... 'šetam' pravougaonik po mogućim terminima
    i drugi klik ga samo ostavi tamo".

    The window was one button, so a click at the bottom of a six-hour area
    placed the lesson at its top: the screen took a large target and threw away
    which part of it was pressed.

    NOW THE AREA IS PAINT AND THE STARTS ARE TARGETS. `.slotfree` above keeps
    `pointer-events: none` and draws the wash; these strips sit inside it, one
    per start the API answered with, each one slot tall and invisible. The one
    under the cursor draws a ghost of the lesson's REAL height from its own
    start - which is exactly the rectangle walking, and the click leaves it
    where the ghost is.

    NO SCRIPT, and a script would not do it better. :hover on the strip is the
    same event a mousemove handler would listen for, and this way it works for
    the keyboard too: the strips are buttons, so Tab walks the same ghost down
    the day.

    `--dur` is set per strip in the markup because it is the lesson's length in
    slots, which the stylesheet cannot know. */
body.admin .slotpick
{
    /*  The lesson's length in slots. Declared here as one and overridden per
        strip in the markup, which is exactly how `.card` handles `--card`:
        the stylesheet owns the DEFAULT and the page owns the VALUE, so a
        strip that somehow arrived without one draws a ghost of one slot
        rather than nothing at all. Also what keeps dead-tokens.sh honest -
        it reads the stylesheets, and a token only ever set in a style
        attribute reads there as never defined. */
    --dur: 1;

    position: absolute;
    left: 2px;
    right: 2px;
    height: var(--slot);
    z-index: 2;

    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    font: inherit;
    color: inherit;
}

/*  THE GHOST. Drawn from the strip's own top, the lesson's height, and only
    while the strip is pointed at or focused. Solid where the window is dashed:
    the window is "somewhere in here is possible", the ghost is "here". */
body.admin .slotpick::before
{
    content: attr(data-label);
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: calc(var(--dur, 1) * var(--slot));

    display: none;
    align-items: flex-start;

    padding: 3px 6px;
    border: 1.5px solid var(--brand);
    border-radius: var(--radius-s);
    background: color-mix(in srgb, var(--brand) 24%, var(--surface));

    color: var(--brand-ink);
    font-size: 11px;
    font-weight: 650;
    line-height: 1.25;
    text-align: left;
    overflow: hidden;
}

body.admin .slotpick:hover::before,
body.admin .slotpick:focus-visible::before
{
    display: flex;
}

/*  The ghost has to be over the window's wash AND over the neighbouring
    strips, or the half of it that overlaps the next start would be painted by
    that one instead. */
body.admin .slotpick:hover,
body.admin .slotpick:focus-visible
{
    z-index: 4;
}

body.admin .slotpick:focus-visible
{
    outline: none;
}

/*  The strip's own text is for a screen reader only: the ghost says the same
    thing in pixels, and a label inside every strip would be a wall of text
    stacked half an hour apart. */
body.admin .slotpick-sr
{
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/*  The forms carry only hidden fields, so they must take no space - without
    display:none, which stops some browsers submitting them. */
body.admin .placeform
{
    margin: 0;
    padding: 0;
    border: 0;
    height: 0;
    overflow: hidden;
}


/* ------------------------------------------------------------------ */
/* The settings screen's reservation vocabulary                        */
/* ------------------------------------------------------------------ */

/*  A READ-ONLY TABLE, and it looks like one. No hover, no cursor, no row
    link: the types come from the desktop and every publish rewrites them, so
    a row that looked clickable would be a promise this screen cannot keep.
    See templates/settings.php for the whole argument. */
.settypes
{
	max-width: 34rem;
	margin: 0.4rem 0 0.2rem;
}

.settypes td
{
	padding: 0.28rem 0.5rem;
	vertical-align: middle;
}

.settype-swatch
{
	width: 1.8rem;
}

/*  The id, last and quiet. It is what a support mail says ("set Color_Id 4 to
    show on the web") and it is not what anybody is reading the row for. */
.settype-id
{
	width: 3rem;
	text-align: right;
	font-variant-numeric: tabular-nums;
	color: var(--ink-soft);
}

/*  A SQUARE, not a disc. The theme picker's dots are choices; these are
    samples of a colour that appears on the grid as a rectangle, and the shape
    is the difference between "pick one" and "this is what you will see".
    --swatch is declared here for the same reason .slotpick declares --dur: a
    custom property used and never defined is one the dead-token check cannot
    tell from a typo. */
.setswatch
{
	--swatch: #cccccc;

	display: block;
	width: 1.1rem;
	height: 1.1rem;
	border-radius: 3px;
	background: var(--swatch);
	box-shadow: inset 0 0 0 1px rgb(0 0 0 / .18);
}

/* ==================================================================== */
/*  ACTIONS AND TOOLS                                                    */
/*                                                                       */
/*  Four small things, and none of them is decoration:                   */
/*                                                                       */
/*    .notice        the three numbers a tool reports when it is done.   */
/*                   It was never styled because until the Generate and  */
/*                   Tools screens nothing on this stylesheet printed    */
/*                   one, so it arrived as a bare paragraph in the same  */
/*                   ink as the body and read like a caption instead of  */
/*                   the answer to the question the user just asked.     */
/*                                                                       */
/*    .toolask       the confirmation. It borrows .refusal's box on      */
/*                   purpose - same shape, same margin - and takes the   */
/*                   red back off, because asking "351 lessons, go?" is  */
/*                   not an error and a red panel would teach people to  */
/*                   read every red panel as noise.                      */
/*                                                                       */
/*    .toollist      the four tools that are not here yet.               */
/*    .toolsyntax    the input language, written down for the first time.*/
/* ==================================================================== */

/*  THE REPORT. A tool that has run says three numbers and this is where
    they land, so it has to be the first thing the eye finds on a page
    that is otherwise a form. A left border rather than a full frame: the
    strip carries the kind - told apart by the word in the text as well
    as by colour, never by colour alone - without boxing body text in a
    tint. */
.notice
{
	margin: 0 0 18px;
	padding: 10px 12px 10px 14px;
	border-left: 3px solid var(--line-strong);
	border-radius: 0 var(--radius-s) var(--radius-s) 0;
	background: var(--surface-2);
	font-weight: 600;
}

.notice-info
{
	border-left-color: var(--ok);
}

/*  Errors keep the danger colour and add the tint, because an error is
    the one notice somebody may be reading with a hand already on the
    back button. */
.notice-error
{
	border-left-color: var(--danger);
	background: color-mix(in srgb, var(--danger) 8%, var(--surface));
}

/*  A THING TO READ BEFORE YOU ACT, WHICH IS NOT AN ERROR - 13 Sep 2026.
 *
 *  Mišo, on the paragraph above "Change the year": "ovakva upozorenja mozes
 *  napisat ali ne bas u jarko crvenoj boji - radi koje mislim da sam napravio
 *  neku gresku".
 *
 *  Exactly the fault. That paragraph explains what changing the year will do
 *  BEFORE anybody has done anything, and it was drawn in `notice-error` - red
 *  text on a red tint, which is the application's way of saying "something
 *  went wrong". Somebody opening the tab read a consequence as an accusation.
 *
 *  The accent, not the danger colour: it is the warm tone this application
 *  already uses to mean "worth your attention" and it is legible on both
 *  schemes. The left strip still carries the kind, and the words still say
 *  what it is - colour is never the only signal here. */
.notice-warn
{
	border-left-color: var(--accent);
	background: color-mix(in srgb, var(--accent) 7%, var(--surface));
	color: var(--ink);
	font-weight: 500;
}

/*  A TURN LINE THAT IS NOT BEING EDITED - 11 Sep 2026.

    Mišo: "tipicno imaju po 16 ili 20 linija po jednom izvođenju i ne smijemo
    dopustiti da se to rasteže po webu dole beskonačno".

    So the table's ordinary state is TEXT. A shut row is one line high, reads
    across like a printed list, and carries no control but the pencil. Twenty
    of them are a table somebody can scan; twenty open ones were a page.

    The open row keeps the height its inputs need - it is one row out of
    twenty, and it is the row the reader is looking at. */
.planlines .line-shut td
{
	padding-top: 4px;
	padding-bottom: 4px;
	white-space: nowrap;
	color: var(--ink);
}

/*  The people on a shut line, as a sentence. It is the one cell that can run
    long - six names on a lab line is real - so this is the cell that wraps and
    the one that gets the width. */
.planlines .line-shut .linewho-shut
{
	white-space: normal;
	color: var(--ink-soft);
	font-size: 12px;
	line-height: 1.35;
}

/*  ONE SIZE DOWN THE WHOLE SHUT ROW - 11 Sep 2026. Mišo: "fontove bi samo
    izjadnacio".

    He was looking at a row that carried three sizes at once: the plan in 12px
    monospace, the lecturers in 12px, and the weeks, the room and the cohort
    count at the table's own 14px. Three sizes across one line makes the line
    read as three things, and the eye keeps re-finding its place. So the row
    sets the size, once, and nothing inside it sets another.

    12px and not 14px because this table is a list of twenty lines, not a page
    of prose - and because the plan is monospace, which runs wider than the
    text beside it at the same nominal size. */
.planlines .line-shut td,
.planlines .line-shut td:first-child,
.planlines .line-shut .planlocked,
.planlines .line-shut .linewho-shut,
.planlines .line-shut a
{
	font-size: 12px;
}

.planlines .line-shut td
{
	line-height: 1.45;
}

/*  The row being edited, marked. With nineteen quiet rows around it the open
    one should be obvious without being loud - a tint and a left edge, the same
    grammar the refusal uses. */
.planlines .line-open td
{
	background: color-mix(in srgb, var(--brand) 4%, transparent);
}

.planlines .line-open td:first-child
{
	box-shadow: inset 3px 0 0 var(--brand);
}

/*  THE STRIP UNDER THE OPEN ROW, which exists to hold one button - see
    templates/courseform.php for why the delete had to leave the action
    cluster. It is part of the open row, so it carries the same tint and the
    same left edge and NO rule between them: two boxes would read as two
    records. */
.planlines .line-foot > td
{
	padding-top: 0;
	padding-bottom: 8px;
	border-bottom: 1px solid var(--line);
}

.planlines .line-open + .line-foot > td,
.planlines .line-refused + .line-foot > td
{
	border-top: 0;
}

/* ==================================================================== */
/*  The plan, week by week - 11 Sep 2026                                 */
/* ==================================================================== */

/*  Mišo: "Hours weeks je sada za sve weeks, pored se moze otvorit details koji
    ce kao u desktopu ponudit upis razlicitog trajanja za svaki week unutar
    period definiranog za turnus".

    "1-16:3" is one range and seven plans in ten are exactly that, so the
    one-line box stays the default. This is the way in for the other three -
    and it is an ADDRESS, `&wk=1`, so the boxes are built only for the line
    that asked. Sixteen of them on every open row would be the same mistake the
    pickers made. */

/*  The way in and the way out, beside the plan. A link and not a button: both
    of them go somewhere, and Back should undo either. */
.planlines .linkish
{
	display: inline-block;
	margin-left: 8px;
	font-size: 11px;
	color: var(--brand);
	text-decoration: none;
	white-space: nowrap;
}

.planlines .linkish:hover
{
	text-decoration: underline;
}

/*  The stored plan while the grid is the editor: shown, because the reader
    needs to see what they are taking apart, and NOT a box, because the boxes
    below are the box now. Softer than .planlocked proper - that one means "the
    desktop wrote something this screen may not touch", which is a different
    and much worse thing to be told. */
.planlines .planlocked-soft
{
	border-bottom-style: dotted;
	cursor: default;
}

/*  ONE ROW, SIXTEEN BOXES. A plan that runs 21 to 36 is sixteen values and no
    table cell is that wide, so the grid gets a row of its own under the line -
    there is room for the same reason the delete has room: exactly one line is
    ever open. */
.planlines .line-weeks > td
{
	padding-top: 2px;
	padding-bottom: 8px;
}

.weekgrid
{
	display: flex;
	flex-wrap: wrap;
	gap: 4px;
}

/*  Week number over its hours, because the reader scans the NUMBERS to find
    the week they mean and then types under it. Side by side, the digits of the
    week and the digits of the hours run together into one string of numbers
    nobody can read at a glance. */
.weekcell
{
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1px;
}

.weekcell-n
{
	font-size: 10px;
	line-height: 1;
	color: var(--ink-soft);
	font-variant-numeric: tabular-nums;
}

/*  Wide enough for "2+1", which is the longest thing in 7% of the estate and
    the shape the hint names. Anything longer scrolls inside the box rather
    than widening every other one. */
.weekgrid .weekhours
{
	width: 5ch;
	height: 26px;
	padding: 0 4px;

	text-align: center;
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: 12px;
}

/*  A week the plan does not mention is tinted, so the shape of the plan is
    visible at a glance - which weeks it runs and which it skips - rather than
    having to be read out of sixteen numbers.

    `[value=""]` MATCHES THE ATTRIBUTE, NOT THE TYPING. It marks the weeks that
    ARRIVED empty and does not follow the cursor, which is the honest thing for
    it to do: this is a picture of what is stored, and what is stored changes
    on save. A live version would need a script, and the screen would be
    telling the reader the plan had already changed when it had not. */
.weekgrid .weekhours[value=""]
{
	background: color-mix(in srgb, var(--ink) 3%, transparent);
}

.weekgrid-hint
{
	margin: 6px 0 0;
	font-size: 11px;
}

/*  DELETE, SAYING SO. Quiet until it is aimed at - the same bargain the icon
    made - but with the word in it, because the reason it moved is that a bare
    X at the end of a row of buttons reads as Cancel. */
.planlines .linedrop
{
	display: inline-flex;
	align-items: center;
	gap: 6px;

	font-size: 12px;
	color: var(--ink-soft);
	background: transparent;
}

.planlines .linedrop:hover,
.planlines .linedrop:focus-visible
{
	color: var(--danger, #c0392b);
	border-color: var(--danger, #c0392b);
	background: color-mix(in srgb, var(--danger, #c0392b) 8%, transparent);
}

/*  A PICKER INSIDE A TABLE ROW - 10 Sep 2026.

    The turn lines put two of these in every row, where a <select> used to sit
    in 4.6em. The widget is unchanged; only its size is. Without a cap the
    twelve rows of a lab course would each be as tall as the staff list and the
    table would stop being a table.

    The list scrolls at about six rows, which is the same bargain the old
    control made: enough to show that more than one can be ticked, short enough
    that the row beside it stays readable. Search is what makes six rows
    sufficient - that is the whole reason it is here. */
.pickset-tight
{
	min-width: 15ch;
}

.pickset-tight .pickset-list
{
	max-height: 8.4em;
	overflow-y: auto;
}

.pickset-tight .pickset-row
{
	font-size: 12px;
	padding: 1px 4px;
}

/*  The "none" row reads as the absence it is, not as another choice with a
    name. Same weight as the rest so it does not become a button. */
.pickset-none-row .pickset-text
{
	color: var(--ink-soft);
}


/* ==================================================================== */
/*  The same picker, shut - 11 Sep 2026                                  */
/* ==================================================================== */

/*  Mišo: "lecturers jos uvijek ne bi stavio u otvorenu listu - ako moze bi tu
    listu otvorio kad zelim kao combo box koji ima search i ticks".

    The search box fixed the LIST being long. It did nothing about the WIDGET
    being tall - eight lines of it, twice per open row - and on a table that is
    the same cost measured down instead of across. So the picker gets a face:
    one line, the names that are ticked, a caret.

    IT IS A <details>, so the panel is markup and not a script: the checkboxes
    inside post whether it is open or shut, and a browser with no JavaScript
    still gets a control that opens, ticks and saves.  */
.pickdrop
{
	position: relative;
	display: block;
}

/*  THE FACE. Shaped like the input it stands in for - same height, same
    border, same radius - because that is what it is: the control you click to
    change who takes this line. */
.pickdrop > summary
{
	display: flex;
	align-items: center;
	gap: 6px;

	min-height: 30px;
	padding: 3px 8px;

	font-size: 12px;
	color: var(--ink);

	background: var(--surface);
	border: 1px solid var(--line-strong);
	border-radius: 6px;

	cursor: pointer;
	list-style: none;
	user-select: none;
}

/*  The disclosure triangle the browser draws for itself, in two spellings -
    Safari still wants the old pseudo-element. We have a caret of our own and
    two of them read as a bug. */
.pickdrop > summary::-webkit-details-marker
{
	display: none;
}

.pickdrop > summary::marker
{
	content: "";
}

.pickdrop > summary:hover
{
	border-color: var(--brand);
}

.pickdrop > summary:focus-visible
{
	outline: 2px solid var(--brand);
	outline-offset: 1px;
}

/*  THE NAMES, ON ONE LINE WHATEVER THERE ARE. Six lecturers on a lab line is
    real; a face that grew to hold all six would be the open list again with
    extra steps. It truncates, and the whole set is one click away. */
.pickdrop-text
{
	flex: 1 1 auto;
	min-width: 0;

	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.pickdrop-caret
{
	flex: 0 0 auto;
	color: var(--ink-soft);
	transition: transform .12s ease;
}

.pickdrop[open] > summary .pickdrop-caret
{
	transform: rotate(180deg);
}

/*  THE PANEL FLOATS, and that is the whole point: a panel that pushed the row
    down would make the table jump every time somebody looked at a line, and
    the row under the cursor would move out from under it. */
.pickdrop-panel
{
	position: absolute;
	z-index: 60;
	top: calc(100% + 3px);
	left: 0;

	min-width: 100%;
	width: max-content;
	max-width: 32ch;

	padding: 6px;

	background: var(--surface);
	border: 1px solid var(--line-strong);
	border-radius: 8px;
	box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 18%, transparent);
}

/*  Inside the panel the list may be taller than it is in a bare row: it is
    over the table now, not in it, and six rows was a concession to the row
    height that no longer applies.

    THE CAP APPLIES WHETHER OR NOT THE PICKER IS COMPACT. `.pickset-tight` is
    the table-row variant and carries its own; a combo outside a table - the
    lecturer's "may teach" list, which is every subject of a branch - had none
    at all, so sixty subjects made a panel taller than the window with the
    Add button under the fold. */
.pickdrop-panel .pickset-list
{
	max-height: 16em;
	overflow-y: auto;
}

.pickdrop-panel .pickset-tight .pickset-list
{
	max-height: 14em;
}

/*  A SUBJECT NAME IS A SENTENCE. 32ch is right over a table cell, where the
    thing being picked is a room or a surname; it truncates "Človeški in
    organizacijski faktorji" to nothing useful. Outside a table there is room,
    so the panel takes it. */
.pickdrop:not(.pickdrop-tight) .pickdrop-panel
{
	max-width: 52ch;
}

/*  A COMBO ON A PHONE STAYS PUT. `.sublist` scrolls sideways at that width
    (see the narrow-screen block near the top of this file), and a sideways
    scroller clips vertically too - so an absolutely positioned panel would be
    cut off at the bottom of the table with no way to reach the rest of it.
    Below that width the panel simply opens in place, which is the behaviour
    the widget had before it learned to shut. */
@media (max-width: 720px)
{
	.pickdrop-panel
	{
		position: static;
		max-width: none;
		box-shadow: none;
		border: 0;
		padding: 6px 0 0;
	}
}

/*  THE ANSWER TO A REFUSAL - 10 Sep 2026, the "delete it anyway" button
    inside the box that has just said what is in the way.

    IT SITS INSIDE THE RED FRAME ON PURPOSE. The counts above it are the
    question, and an answer that had moved to the bottom of the page would be
    a second control the reader has to connect back to a sentence they have
    scrolled past. The rule the box already sets is not broken: the frame is
    the refusal, the button is the destructive act, and only the button is
    red-filled.

    The rule above it draws a line rather than a margin - this is a different
    kind of thing from the sentences it follows, and the eye should be told
    so before the finger arrives at a delete. */
.refusal-answer
{
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px;
	margin-top: 12px;
	padding-top: 12px;
	border-top: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
}

/*  The hint beside the button, not under it: on one line the two read as a
    single sentence with a control in it, which is what it is. It wraps below
    on a narrow screen because flex-wrap says so, and that is fine - the order
    is still button first, consequence second. */
.refusal-answer .refusal-hint
{
	margin: 0;
}

/*  THE QUESTION. .refusal gives it the frame; this takes the red out of
    the frame and puts the neutral line back. The submit button inside is
    still .btn-danger - the button is the destructive act, the box around
    it is not. */
.toolask
{
	border-color: var(--line-strong);
	background: var(--surface-2);
}

/*  The form inside the question sits closer than a form of its own
    would: the buttons belong to the sentence above them, not to the
    page. */
.toolask .form
{
	margin-top: 12px;
}

.toolask .form-actions
{
	margin-top: 0;
}

/* ------------------------------------------------------------------ */
/*  The four that run elsewhere                                        */
/* ------------------------------------------------------------------ */

/*  Named and described, with no button - which is the point. A tool left
    off the screen until it works teaches nobody that it exists; a tool
    listed with a dead button teaches everybody to distrust the buttons.
    So: a list, no controls, and one line saying where it runs today. */
.toollist
{
	margin: 0 0 8px;
	padding: 0;
	list-style: none;
}

.toollist li
{
	padding: 8px 0;
	border-bottom: 1px solid var(--line);
}

.toollist li:last-child
{
	border-bottom: 0;
}

/*  The hint on its own line under the name. .sub is inline everywhere
    else on this stylesheet, and here it is the second half of a two-line
    entry, so it is turned into a block for this list only. */
.toollist .sub
{
	display: block;
	margin-top: 2px;
}

/* ------------------------------------------------------------------ */
/*  The input language                                                 */
/* ------------------------------------------------------------------ */

/*  A reference table, not a list of records: narrow, because nine short
    keys stretched across a wide screen put a hand's width between the
    key and what it means. */
.toolsyntax
{
	max-width: 46rem;
	margin-bottom: 8px;
}

.toolsyntax td
{
	padding: 5px 10px;
	vertical-align: top;
}

/*  The key column is sized to its widest member ("3(AB)") and stays put
    when a description wraps, so the keys read down the page as a column
    and not as a ragged left edge. */
.toolsyntax-key
{
	width: 6.5rem;
	white-space: nowrap;
}

.toolsyntax-key code
{
	padding: 1px 5px;
	border: 1px solid var(--line);
	border-radius: 3px;
	background: var(--surface-2);
	font-size: .86rem;
}

/*  THE ROOM PICKER. Wide, for the same reason the schedule's entity picker
    is: a room here is "UL FMF, Jadranska 19, VFP", and a select truncated to
    the width of the week boxes next to it turns choosing a room into
    guessing which of four "UL FMF, Jadranska 19, ..." is the right one. */
.toolroom
{
	min-width: 18rem;
	max-width: 28rem;
}

/* ==================================================================== */
/*  THE DRAWER ARRIVES, RATHER THAN APPEARING                            */
/*                                                                       */
/*  Mišo, 7 Sep 2026: "desni details trail nek mi se dosece smoothly i    */
/*  tako i ode".                                                         */
/*                                                                       */
/*  IT SLIDES IN. IT CANNOT SLIDE OUT, and that is worth writing down    */
/*  rather than leaving somebody to wonder why half of it works.         */
/*                                                                       */
/*  Opening the drawer is a NAVIGATION - the card is a link, `?ev=` is   */
/*  the address, and the screen is a fresh document. An entry animation  */
/*  therefore runs exactly when it should: the new page arrives and the  */
/*  panel comes in from the right edge.                                  */
/*                                                                       */
/*  Closing is a navigation too, and on the page that comes back the     */
/*  drawer IS NOT IN THE DOCUMENT. There is no element left to animate.  */
/*  CSS cannot transition something that was never rendered, and the     */
/*  only way to get the exit is to keep the panel in the markup always   */
/*  and toggle it with script - which would put JavaScript on the one    */
/*  screen this application has kept free of it, for a quarter of a      */
/*  second of movement. Not worth it.                                    */
/*                                                                       */
/*  So: it arrives smoothly and it leaves at once. That reads as         */
/*  deliberate rather than broken as long as the arrival is quick - a    */
/*  long entry with an instant exit is what feels wrong.                 */
/* ==================================================================== */

@keyframes drawer-in
{
	from
	{
		/*  Just off its own edge, not off the viewport: a panel that
		    travels the whole screen width reads as a page turn. */
		transform: translateX(12%);
		opacity: 0;
	}

	to
	{
		transform: translateX(0);
		opacity: 1;
	}
}

.drawer
{
	/*  180ms, and the ease-out is the half that matters: fast at the
	    start, settling at the end, which is how a thing with weight
	    stops. Linear reads as a slide projector. */
	animation: drawer-in 180ms cubic-bezier(.2, .7, .3, 1);
}

/*  SOMEBODY WHO HAS ASKED FOR NO MOTION GETS NONE. It is one line here and
    it is the difference between a nice touch and a screen that a person
    with vestibular trouble cannot use. */
@media (prefers-reduced-motion: reduce)
{
	.drawer
	{
		animation: none;
	}
}

/* ------------------------------------------------------------------ */
/*  SETTINGS > GENERATE - the school's generation parameters.
 *
 *  Mišo, 7 Sep 2026: "a Generate tab - pa sve one stu su unutar desktopa za
 *  generiranje...". The tab carries the desktop's whole Generate page, so it
 *  is long by nature: thirteen numbers, twenty switches and three windows.
 *  What it must not also be is WIDE - a full-width input for a two-digit
 *  number reads as a text field somebody is meant to write a sentence in, and
 *  a column of them is how a settings page stops being skimmable.
 *
 *  Two classes, both narrowing, neither introducing a new layout: the rest of
 *  the tab is the same .field / .check / .form-actions the preferences tab
 *  uses, so there is one form idiom on this screen and not two.
 */

/*  A NUMBER THAT IS A NUMBER. .numbox alone is 3.4rem - sized for 1..52 on
    the availability bar - and the generation numbers reach five digits, so
    this widens it just enough rather than defining a second box. */
.field-narrow .numbox
{
    width: 5.2rem;
}

.field-narrow .input
{
    max-width: 14rem;
}

/*  THE TWO ENDS OF A WINDOW, SIDE BY SIDE, because they are one answer.
    Stacked, "from" and "to" read as two independent settings and somebody
    changes one - which is exactly the request the API refuses as a reversed
    window. Beside each other with a dash between them, the pair is obviously
    a pair. Wraps on a narrow screen rather than overflowing. */
.rangepick
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
}

.rangepick .input
{
    width: auto;
    min-width: 6.5rem;
}

/*
 * EACH LABEL BESIDE ITS OWN BOX, WITHOUT LOSING THE COLUMN - 12 Sep 2026.
 * Mišo: "daj nekako from week i to week ka svojim poljima blize", and then,
 * looking at the whole tab: "poravnaj malo stvari".
 *
 * Those two pull in opposite directions and both are right, so the answer is
 * that the two labels in a range are not the same KIND of label.
 *
 *   THE FIRST ONE IS A ROW LABEL. It belongs to the column that runs down the
 *   form - "From week" over "Filter aid" over "Room" - and it keeps the 140px
 *   reserve `.field-inline` gives it, which is what makes every box on the
 *   tab start at the same x. Take that away and the range's box jumps 50px
 *   left of the one under it, which is the alignment he asked for, broken.
 *
 *   THE SECOND ONE IS INSIDE THE ROW. "To week" has nothing above or below it
 *   to line up with; its only job is to name the box it touches. It gets the
 *   width of its own words and sits right against its box.
 *
 * AND THE SPACE BETWEEN THE PAIRS IS BIGGER THAN THE SPACE INSIDE THEM, which
 * is the whole of what makes them read as two pairs rather than four loose
 * objects in a row.
 */
.rangepick .field-inline
{
    gap: .45rem;
}

.rangepick > .field-inline + .field-inline
{
    margin-inline-start: 1.3rem;
}

.rangepick > .field-inline + .field-inline .field-label
{
    min-width: 0;
}

/*
 * AND THE FIRST LABEL LETS GO TOO, WHEREVER THERE IS NOTHING TO LINE UP WITH
 * - 12 Sep 2026, the evening. Mišo, looking at the Copy tab: "to polje za
 * from week nek ide lijevo do labele".
 *
 * The note above is not wrong; it is over-applied. It keeps the 140px reserve
 * on the first label so the range's box starts at the same x as the boxes
 * above and below it - and on the COPY tab there are no boxes above or below
 * it. That form is the range and a tick box, nothing else. So the reserve
 * bought alignment with nothing and spent 50px of gap between a label and the
 * box it names, which is what he is looking at.
 *
 * ON THE ROOM AND DELETE TABS IT STILL BUYS SOMETHING: both have a
 * `.field-inline` of their own - the room picker, the filter aid - directly
 * above the range, and taking the reserve away there breaks the column that
 * the earlier note was written to protect.
 *
 * SO THE RULE IS THE CONDITION ITSELF, asked of the form: keep the reserve
 * only where the form holds a `.field-inline` OUTSIDE the range for it to
 * line up with. `:has` is the one selector that can ask that, and the
 * fallback is the right way round - a browser without it gets the narrow
 * label everywhere, which is the look he asked for, on every tab.
 *
 * `> .field-inline` and not a bare descendant: the two labels INSIDE the
 * range must not count as something to align with, or every range would
 * match its own condition and nothing would change.
 */
.rangepick > .field-inline:first-child .field-label
{
    min-width: 0;
}

/*
 * AND THE COLUMN LOSES, EVERYWHERE - 13 Sep 2026, the third pass on this and
 * the last.
 *
 * The rule that stood here kept the 140px reserve on the first label wherever
 * the form had another `.field-inline` to line up with. That is defensible on
 * paper and it produced, on the Delete tab, exactly the thing he then
 * photographed: "lijevi labels daleko od inputa, desni ne". FROM WEEK and
 * FILTER AID sat a hand's width from their boxes while TO WEEK, inside the
 * same row, touched its own. One row, two spacings, no reason a reader could
 * see.
 *
 * A LABEL BELONGS TO ITS BOX. That is the thing being asked for twice now,
 * and between the two goods - every box starting at the same x, or every
 * label touching what it names - the second is the one somebody notices. The
 * column was a tidiness nobody had asked for; the gap was a question in the
 * middle of a form.
 *
 * So the reserve is gone from the whole `.rangepick` idiom and the rule above
 * is the only one left. `.field-inline` elsewhere - the page-size rows it was
 * written for - keeps its 140px, because there the labels really are a column
 * and there is no box beside them going short.
 */
.rangepick .field-label
{
    min-width: 0;
}

/*  AND THE SAME ON THE TOOLS SCREEN, where the label he photographed beside
    the range was FILTER AID - a plain `.field-inline`, not part of any range,
    so the rule above cannot reach it. One class on that page's <main> rather
    than the same declaration on sixteen forms. */
.toolspage .field-inline .field-label
{
    min-width: 0;
}

/*  A WEEK NUMBER IS TWO DIGITS. 6.5rem is the minimum for the date boxes this
    class was written for; a box that holds "37" and is sized for "31.12.2026"
    is the same fault as the label reserve above, measured across instead of
    between. Only the numbers narrow - .numbox is what the templates put on
    them. */
.rangepick .input.numbox
{
    width: 4.4rem;
    min-width: 0;
}

/*
 * AND THE ROWS GET AIR - "samo malo razdvoji linije - par pixela barem
 * prostora između", the same afternoon.
 *
 * `.form` is app.css's and sets no vertical rhythm, so on the tools tabs the
 * range sat directly on the room chooser above it and the checkbox directly
 * under it: three controls with no space between, which reads as one block
 * somebody has to untangle rather than as three questions.
 *
 * ON THE CHILDREN AND NOT AS A FLEX `gap`, because these forms are ordinary
 * block flow - the fields are labels and divs - and a gap on a block
 * container does nothing. Margins collapse with the headings' own 22px, so a
 * section still opens with the space its heading asks for rather than with
 * both.
 */
.setform > * + *
{
    margin-block-start: 12px;
}

/*  THE ROOT PAGE'S ONE INSTRUCTION.
 *
 *  A little air above it and a readable code span, so the address shape is the
 *  thing the eye lands on. Everything else on that page is two short lines;
 *  without this the sentence sits flush against them and reads as more of the
 *  same prose rather than as the way in. */
.landing-way
{
    margin-top: 1.1rem;
}

.landing-way code
{
    padding: 0.05rem 0.2rem;
    border-radius: 4px;
    background: var(--surface-2, rgba(127, 127, 127, 0.14));
    font-size: 0.95em;
}

.landing-way em
{
    font-style: normal;
    opacity: 0.75;
}

/* ------------------------------------------------------------------ */
/*  A SETTINGS SCREEN AS A MATRIX, NOT AS A COLUMN.
 *
 *  Mišo, 7 Sep 2026: "gledam generation parameters i jos neke druge - protezu
 *  se po webu dugo dole - ako ikako moze nekako da su u ljepsoj matrici ili
 *  dupli tab ili tako nesto".
 *
 *  The Generate tab is three windows, eleven numbers and twenty switches. In
 *  one column that is about two thousand pixels, and a setting nobody scrolls
 *  to is a setting nobody knows is there.
 *
 *  NOT SUB-TABS. Tabs shorten a page by HIDING it, and this screen's value is
 *  that the rules a run will use are all visible at once. They would also put
 *  a click in front of a setting somebody is already looking for.
 *
 *  `auto-fit` AND NO BREAKPOINTS. The grid takes as many columns as fit at
 *  320px each: three on a desk, two on a laptop, one on a tablet, and it is
 *  right on the next screen size nobody has tested. A media query would be a
 *  list of screen widths to maintain and to be wrong about.
 *
 *  GENERAL ON PURPOSE - "i jos neke druge". These four classes carry no
 *  Generate-specific rule, so the next long settings screen becomes a matrix
 *  by wrapping its groups in the same two elements.
 */
/* ------------------------------------------------------------------ */

.setgrid
{
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 18px;
    align-items: start;
    margin-top: 14px;
}

.setcard
{
    /*  A CARD, AND ONLY JUST. A border and a little padding is enough to say
        "these belong together"; a filled panel with a shadow would turn a
        settings page into seven boxes shouting at each other.

        THE BORDER WAS INVISIBLE, AND THAT WAS THE WHOLE COMPLAINT - 13 Sep
        2026. Mišo, twice: "moze u dvije kolone na primjer ali da ih lijepo
        odvojis linijama", then "malo da se sredi u sekcije i to -
        settings/generiranje". The sections were already here and had been
        since 7 Sep; what was missing is that nobody could SEE them.

        MEASURED, not guessed. The card was `1px solid var(--line)`, and on the
        dark themes --line is #262c36 against an --editwrap surface of #1f242d:
        a seven-value difference, which is nothing. Rendered in a headless
        browser against this stylesheet it comes out as one undivided panel -
        which is precisely what his screenshot shows, and also why the text
        near a card's right edge read as "cut off at the panel edge": the edge
        it stopped at was the card's, and the card was not drawn.

        --line-strong (#3a4250 dark, #c9d0da light) is the token that exists
        for exactly this and is already used on .tab for the same reason. The
        faint fill is the second half: a section that is a shade darker than
        the page reads as an inset panel rather than as a rectangle drawn on
        one, and it costs nothing on the light theme where --surface is white. */
    border: 1px solid var(--line-strong);
    background: color-mix(in srgb, var(--surface) 60%, transparent);
    border-radius: 8px;
    padding: 4px 16px 16px;
    min-width: 0;
}

/*  The heading inside a card does not need the space it needs on a page: the
    card's own top edge is already the separation, so setgroup-apart's margin
    would push every title away from the box it titles. */
.setcard > .setgroup
{
    margin-top: 10px;
    margin-bottom: 6px;

    /*  AND IT IS THE SECTION'S NAME, so it is read at full strength. On a page
        .setgroup is --ink-soft because it is one quiet marker among many; in a
        card it is the only title the box has, and a title nobody's eye lands
        on is a section that still does not read as one. */
    color: var(--ink);
    font-size: 11.5px;
}

/*  Fifteen switches. Given one column it is a card four times the height of
    every other one, which breaks the grid's rhythm and is the shape the whole
    change was made to remove. */
.setcard-wide
{
    grid-column: span 2;
}

/*  ...and inside it, the switches themselves go two across, WITH A LINE
    BETWEEN THEM - 13 Sep 2026.

    Mišo, on the Generate tab: "pozdravljam neko uređivanje mnogo opcija sa
    dekoracijama - može u dvije kolone na primjer ali da ih lijepo odvojiš
    linijama i poravnaš i slično", and again the next day: "malo da se sredi u
    sekcije i to - settings/generiranje".

    COLUMNS AND NOT A GRID, and the reason is the line. This was
    `grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))`, which put
    the switches in two columns and could not draw anything between them: CSS
    grid has no rule between tracks, and faking one with a border on
    `:nth-child(even)` is only correct while the track count happens to be two.
    Multi-column layout has `column-rule`, which is exactly this line and is
    drawn by the browser wherever the break actually falls.

    `columns: 250px 2` is a WIDTH AND A CAP. The count is a maximum, not a
    demand: the wide rules card is 1100px and would otherwise take four
    columns, which is a wall rather than a list, and a narrow card or a phone
    falls to one column and the rule disappears with it because there is
    nothing left to separate.

    `break-inside: avoid` on the rows, so a two-line label is never split
    across the column break - which is the one way a multi-column list can
    read worse than a grid. */
.setchecks
{
    columns: 250px 2;
    column-gap: 30px;
    column-rule: 1px solid var(--line);
}

/*  AND A LINE BETWEEN THE OPTIONS THEMSELVES, quiet enough to be a rule and
    not a table. Every row carries it on top; the first row of each column sits
    under the card's own heading, where a line reads as the heading's underscore
    rather than as a stray. */
.setchecks > .check
{
    break-inside: avoid;
    border-top: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
    padding-block: 7px;

    /*  THE LABEL WRAPS. ALWAYS.

        Mišo's screenshot of 13 Sep shows "Generate also first weekend da"
        stopping dead at the card's edge with no second line. It could NOT be
        reproduced in a standalone render of this exact markup against this
        exact stylesheet - measured with a headless browser at four widths, and
        the span wrapped correctly at every one - so rather than leave a
        stylesheet hunt unfinished on a screen he is testing right now, the
        property is stated outright here. `white-space: normal` beats an
        inherited `nowrap` from wherever it is coming from, and
        `overflow-wrap: anywhere` handles the one thing wrapping cannot: a
        single word longer than the column, which a translated label can be.

        `min-width: 0` on the flex item is the other half - without it a flex
        child refuses to shrink below its longest word even when it is allowed
        to wrap. */
    min-width: 0;
}

.setchecks > .check > span
{
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
}

/*  A card is already narrow, so a field inside one takes the width it is
    given rather than the 14rem the page version caps at. */
.setcard .field-narrow .input
{
    max-width: none;
}

/*  ONE COLUMN, AND THE WIDE CARD STOPS BEING WIDE. `span 2` on a
    single-column grid is not an error - the browser clamps it - but saying it
    here keeps the intent readable rather than relying on that. */
@media (max-width: 720px)
{
    .setcard-wide
    {
        grid-column: auto;
    }
}


/* ======================================================================
   THE SUBJECTS TAB on the lecturer screen.

   A LIST OF CARDS, NOT A TABLE. A table wants every row to answer the same
   columns; here a subject has one part or seven, and the parts are the
   interesting half. Cards let the subject name stay the heading it is and
   the parts sit under it as what they are - a short list of three-word
   facts. It also collapses to one column on a phone without a horizontal
   scroller, which a five-column table does not.
   ====================================================================== */

.coursetab
{
	margin-top: 14px;
}

.subjcounts
{
	margin: 0 0 4px;
	color: var(--ink);
}

/*  The split - "may teach 27, does teach 19" - is context for the two
    numbers before it, not a third number of its own. Dimmed so the eye
    reads the totals first and comes back to this only if it wants to. */
.subjsplit
{
	/*  --ink-soft, and it used to say --muted WITH NO FALLBACK.

	    That token is not defined anywhere in this product, and a custom
	    property with no fallback and no definition makes the declaration
	    invalid at computed-value time - so `color` fell back to INHERIT and
	    this line rendered at exactly the weight of the numbers above it. The
	    comment said "dimmed so the eye reads the totals first" and the screen
	    did the opposite. Found 10 Sep 2026 by tools/dead-tokens.sh. */
	color: var(--ink-soft);
	margin-left: 4px;
}

/*  THE ONE THING ON THIS TAB THAT IS A PROBLEM rather than information.
    Bordered on the left rather than filled: a filled warning band above a
    list of subjects reads as "this lecturer is wrong", and the lecturer is
    not wrong - the school's suitable-tutor list is short. */
.subjwarn
{
	margin: 8px 0 14px;
	padding: 8px 12px;
	border-left: 3px solid var(--warn, #b45309);
	background: var(--warnbg, rgba(180, 83, 9, .06));
	border-radius: 0 6px 6px 0;
}

.subjlist
{
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
	gap: 14px;
	align-items: start;
	margin-top: 12px;
}

.subjcard
{
	border: 1px solid var(--line);
	border-radius: 8px;
	padding: 10px 14px 12px;
	min-width: 0;
}

.subjname
{
	margin: 0 0 8px;
	font-size: 1rem;
	line-height: 1.35;

	/*  A subject name at a Slovenian university runs to sixty characters
	    and there is nothing to be done about it but let it wrap. */
	overflow-wrap: anywhere;
}

/*  The code, or - where the school has none and two subjects share a name -
    the id. Small and dim: it is a disambiguator, not a label anybody reads
    on purpose. */
.subjcode
{
	/*  The stack written out, not var(--mono, ...): this product has no
	    --mono token, so the fallback was the only thing that ever rendered
	    and the token read as a theme hook that does not exist. Everywhere
	    else in this sheet - .rolekey, .placenote-why - writes the stack, so
	    this now matches them and there is one spelling of "monospaced here".

	    --ink-soft for the same reason as .subjsplit above: --muted with no
	    fallback made this line inherit its parent's colour, so the code
	    beside a subject name was as loud as the name. */
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: .8em;
	font-weight: normal;
	color: var(--ink-soft);
	margin-left: 6px;
}

.subjparts
{
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 4px;
}

.subjpart
{
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 4px 10px;
	padding: 3px 0;
	border-top: 1px solid var(--line);
}

.subjpart:first-child
{
	border-top: 0;
}

/*  The type - Lecture, Seminar, Lab - is what identifies the part, so it
    is the one thing here set in the body colour. */
.subjtype
{
	font-weight: 600;
	min-width: 6rem;
}

.subjstate,
.subjturns
{
	/*  --ink-soft. The third and last site where this tab read a --muted
	    that does not exist and gave no fallback, so the state and the turn
	    count sat at the same weight as the subject line they qualify. */
	color: var(--ink-soft);
	font-size: .9em;
}

/*  THE THREE STATES, and only one of them is coloured.

    "teaches" is the ordinary case and gets nothing - colouring the normal
    case leaves no colour for the abnormal one. "may teach" is dimmed
    because it is a smaller fact than teaching. "teaches but is not on the
    suitable list" is the finding, and it is the only thing on the tab that
    earns a colour. */
.subjpart-may .subjstate
{
	opacity: .75;
}

.subjpart-unlisted .subjstate
{
	color: var(--warn, #b45309);
	font-weight: 600;
}

/* ==================================================================== */
/*  "May teach", written from the lecturer's side - 11 Sep 2026          */
/* ==================================================================== */

/*  THE CROSS ON A "MAY TEACH" LINE. A form, because it writes - but it has to
    sit inside the row like a chip and not break the line it is on, which is
    what a <form> does by default. */
.subjdrop
{
	display: inline;
	margin: 0 0 0 auto;
}

.subjx
{
	display: inline-flex;
	align-items: center;
	justify-content: center;

	width: 20px;
	height: 20px;
	padding: 0;

	color: var(--ink-soft);
	background: transparent;
	border: 1px solid transparent;
	border-radius: 5px;
	cursor: pointer;

	/*  QUIET UNTIL AIMED AT, like the delete on a turn line. A row of these
	    down a card, each one permanently red, would read as a warning about
	    the subjects rather than as an offer to change one. */
	opacity: 0;
	transition: opacity .12s ease;
}

/*  Shown on hover, and ALWAYS to the keyboard. `opacity: 0` hides it from the
    eye and not from the tab order, so a reader who cannot use a mouse would
    otherwise be tabbing onto a control they cannot see. */
.subjpart:hover .subjx,
.subjx:focus-visible
{
	opacity: 1;
}

.subjx:hover,
.subjx:focus-visible
{
	color: var(--danger, #c0392b);
	border-color: var(--danger, #c0392b);
	background: color-mix(in srgb, var(--danger, #c0392b) 8%, transparent);
}

/*  The picker under the cards. Set apart from them by a rule rather than by
    air: it is a different kind of thing - the cards report, this one acts -
    and the section heading alone did not say so. */
.maypick
{
	margin-top: 18px;
	padding-top: 14px;
	border-top: 1px solid var(--line-strong);
}

/*  The combo is as wide as the column it sits in, up to a point: a picker
    stretched to a 1130px page reads as the page's own control rather than as
    one field of a form. */
.mayform .pickdrop
{
	max-width: 46ch;
}

.mayform .editbar
{
	margin-top: 10px;
}


/*  THE WARNING COLOUR IN BOTH SCHEMES.

    There is no --warn token in the palette - only --danger, and this is not
    danger: nothing here is broken or about to be lost, the school's suitable
    tutor list is simply shorter than reality. So the two values are set
    here, in one place, the same way the scheme switch does it: a class for
    the person who chose, and a media query with :not(.mode-light) for the
    machine that chose. Amber at 600 on light, at 400 on dark - a colour that
    reads on paper is too dark to read on #101318.

    If --warn ever joins the palette, these three blocks come out and the two
    var() fallbacks above become the whole answer. */
body.mode-dark .subjwarn,
body.mode-dark .subjpart-unlisted .subjstate
{
	--warn:   #fbbf24;
	--warnbg: rgba(251, 191, 36, .10);
}

@media (prefers-color-scheme: dark)
{
	body.admin:not(.mode-light) .subjwarn,
	body.admin:not(.mode-light) .subjpart-unlisted .subjstate
	{
		--warn:   #fbbf24;
		--warnbg: rgba(251, 191, 36, .10);
	}
}


/* ======================================================================
   THE STUDENTS ROSTER.

   The groups column is the one that decides the shape. Every student on the
   measured school is in one to four groups and almost none of them has a
   usable Branch_Id, so the groups ARE the student's place in the school and
   they have to be readable at a glance across fifty rows - which means chips
   that wrap inside their cell rather than a comma-separated sentence that
   pushes the table sideways.
   ====================================================================== */

.studchips
{
	display: flex;
	flex-wrap: wrap;
	gap: 4px;
	align-items: center;
}

/*  A group chip is a LINK - it narrows the list to that group. So it carries
    the affordance a chip normally does not: it changes on hover. Everything
    else about it matches .edittable .chip, which is where the plain ones are
    defined; only the parts that differ are set here.

    margin-left is cleared because these live in a flex row with its own gap;
    inherited margins on a flex child are how a tidy row becomes a ragged
    one. */
.chip-group
{
	display: inline-block;
	margin-left: 0;
	padding: 1px 7px;
	border: 1px solid var(--line);
	border-radius: 10px;
	background: var(--surface);
	color: var(--ink-soft);
	font-size: .72rem;
	white-space: nowrap;
	text-decoration: none;
}

.chip-group:hover,
.chip-group:focus-visible
{
	border-color: var(--line-strong);
	color: var(--ink);
}

/*  THE STAR IS THE WHOLE POINT OF SHOWING MEMBERSHIPS HERE.

    A membership the web enrolled carries Online = 1 and SURVIVES the
    planner's next publish; one the desktop owns does not. That is the single
    most consequential fact on this screen and it has no other home - so it
    is a mark on the chip rather than a column, because it belongs to the
    membership and not to the student.

    Nothing on temp carries it yet. It will, the moment enrolment lands. */
.chip-star
{
	margin-left: 3px;
	color: var(--accent, #768ed2);
}

/*  A FILTER THAT IS ON MUST SAY SO AND MUST BE REMOVABLE. Clicking a group
    chip narrows the roster; without this line the only way back is the
    browser's Back button, which is a memory rather than a control. */
.filternote
{
	display: flex;
	align-items: center;
	gap: 10px;
	margin: 10px 0 0;
	color: var(--ink-soft);
}

.crumb
{
	margin: 0 0 10px;
}

/* ------------------------------------------------------------------ */
/* One student                                                         */
/* ------------------------------------------------------------------ */

.studpanel
{
	border: 1px solid var(--line);
	border-radius: 8px;
	padding: 4px 18px 18px;
	max-width: 46rem;
}

.studname
{
	font-size: 1.25rem;
	margin: 14px 0 10px;
	overflow-wrap: anywhere;
}

/*  A DEFINITION LIST IN TWO COLUMNS, not a form with disabled inputs. A
    disabled input says "you could change this if something were different",
    which is the wrong sentence: nothing about this record is editable here
    and pretending it nearly is would be a worse lie than saying so once at
    the bottom. */
.studfacts
{
	display: grid;
	grid-template-columns: max-content 1fr;
	gap: 4px 18px;
	margin: 0 0 6px;
}

.studfacts dt
{
	color: var(--ink-soft);
	font-size: .85rem;
}

.studfacts dd
{
	margin: 0;
	overflow-wrap: anywhere;
}

.studsub
{
	font-size: .95rem;
	margin: 18px 0 6px;
	padding-top: 12px;
	border-top: 1px solid var(--line);
}

.studsubs
{
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 3px;
}

/*  A student panel is narrow and its lists are short, so a subject is one
    line with its code trailing - the same .subjcode the lecturer's Subjects
    tab uses, deliberately, because it is the same fact wearing the same
    clothes on two screens. */
.studsubs li
{
	overflow-wrap: anywhere;
}

.muted
{
	color: var(--ink-soft);
}


/*  AN EMPTY ANSWER THAT IS NOT A TABLE CELL.

    app.css has .empty-state for the dash inside a table with no rows. These
    two are different sentences: the Subjects tab and the student panel say
    "there is nothing here, and here is why that is normal" in prose, outside
    any table. Dimmed and given room, so it reads as an answer rather than as
    a gap where content failed to load. */
.emptynote
{
	margin: 6px 0 0;
	color: var(--ink-soft);
	max-width: 46rem;
}


/* ------------------------------------------------------------------ */
/* Enrolment - the one control on the students screen                  */
/* ------------------------------------------------------------------ */

/*  A CHIP AND ITS CROSS ARE ONE THING and must not be split by a line
    break: the cross belongs to the chip on its left and to nothing else.
    inline-flex keeps them together while the row around them still wraps. */
.chipwrap
{
	display: inline-flex;
	align-items: center;
	white-space: nowrap;
}

/*  A form is a block by default and would put every chip on its own line.
    It exists at all because this screen has no JavaScript: three small
    forms are three buttons that work with scripting off. */
.chipdrop
{
	display: inline;
	margin: 0;
}

/*  QUIET UNTIL IT IS WANTED. A removal control that shouts sits next to
    every group a student is in and turns a list into a row of warnings; one
    that only darkens under the pointer is found the moment somebody looks
    for it and is invisible the rest of the time.

    It never appears on a membership the planner's document owns - that is
    decided in the template, because a cross that refuses is worse than no
    cross. */
.chipx
{
	border: 0;
	background: none;
	cursor: pointer;
	padding: 0 3px;
	margin-left: 2px;
	font-size: 1rem;
	line-height: 1;
	color: var(--ink-soft);
	opacity: .55;
}

.chipx:hover,
.chipx:focus-visible
{
	opacity: 1;
	color: var(--danger, #b42318);
}

.studchips-edit
{
	margin: 0 0 10px;
	gap: 6px;
}

.enrolform
{
	display: flex;
	flex-wrap: wrap;
	align-items: end;
	gap: 10px;
	margin: 10px 0 4px;
}

.enrolpick
{
	display: flex;
	flex-direction: column;
	gap: 3px;
}

/*  A school can have hundreds of groups and their names are long. Bounded
    so the picker does not push the button off a narrow panel, and allowed
    to shrink on a phone. */
.enrolpick .input
{
	min-width: 12rem;
	max-width: 22rem;
}


/* ------------------------------------------------------------------ */
/* Generation constraints - the course's third tab       11 Sep 2026   */
/* ------------------------------------------------------------------ */

/*  TWO COLUMNS AND NOT A LIST. Ten tick boxes one per line is a tab you
    scroll; ten in two columns is a tab you read. The desktop puts the same
    ten on one dialog and a planner recognises the shape.

    auto-fit rather than a fixed two, so the same rule gives one column on a
    phone without a second breakpoint. */
.cngrid
{
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 8px 22px;
}

/*  The long sentences - "Many students attend final exam for this course -
    plan additional rooms" - get the full width rather than being squeezed
    into a column and wrapping to three lines. */
.cnwide
{
    grid-template-columns: 1fr;
}

.cnbox
{
    display: flex;
    align-items: flex-start;
    gap: 9px;
    padding: 5px 6px;
    border-radius: var(--radius-s, 6px);
    cursor: pointer;
    line-height: 1.35;
}

.cnbox:hover
{
    background: var(--surface-2, rgba(0, 0, 0, .03));
}

/*  The box does not shrink when the label wraps, and it sits on the first
    line of the label rather than in the middle of a two-line one. */
.cnbox input[type="checkbox"]
{
    flex: 0 0 auto;
    margin: 2px 0 0;
    width: 15px;
    height: 15px;
    cursor: pointer;
}

.cnbox span
{
    flex: 1 1 auto;
}

/*  A ticked rule is a thing that will change what generation does, so it is
    worth being able to see which ones are on without reading every label. */
.cnbox input[type="checkbox"]:checked + span
{
    font-weight: 600;
    color: var(--ink, inherit);
}


/*  The constraint panel, wherever it hangs - under an execution type's
    heading, or under one line of the plan. One block of rules because they
    are the same thing at two levels, and looking different would suggest
    they were not. */
body.admin .cnpanel
{
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px 14px;
    margin: 0 0 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-m);
    background: var(--surface-2, rgba(0, 0, 0, .02));
}

/*  Inside a table cell it is already inset by the row, so it loses the
    outer margin and picks up the row's full width. */
body.admin .line-cn > td
{
    padding: 0;
    background: var(--surface-2, rgba(0, 0, 0, .02));
}

body.admin .line-cn .cnpanel
{
    margin: 0;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
}

/*  A LABEL AND ITS ROW OF BOXES ON ONE LINE - the days of the week, the
    equipment. Both are short words that belong together as a set, and a
    two-column grid would have split them down the middle. */
body.admin .cnprops
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 14px;
}

body.admin .cnproplabel
{
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-soft);
    margin-right: 4px;
}

/*  A weekday is two or three letters; without this each one takes a grid
    column as wide as "Force free day after". */
body.admin .cnday
{
    padding: 4px 6px;
}

/* ==================================================================== */
/*  A DAY THE LINES DISAGREE ABOUT - the course Timing section, 14 Sep   */
/* ==================================================================== */

/*  THE THIRD STATE A CHECKBOX CANNOT HOLD. On the course tab these seven
    boxes summarise every execution line, and a course whose lecture runs
    Monday and whose lab runs Thursday has days that are neither on nor off.
    Such a day is drawn CLEAR - because ticking it is what turns it on
    everywhere - and marked, because a clear box that means "leave this alone"
    and a clear box that means "off" must not look the same.

    A DASHED OUTLINE AND THE WORD, not a colour alone. Colour says "something
    is different here"; only the word says what, and the difference between
    those two is whether somebody trusts the Save button. The word is inside
    the label, so it travels with the box on a narrow screen. */
body.admin .cnday-mixed
{
    border-style: dashed;
    border-color: color-mix(in srgb, var(--ink-soft) 55%, transparent);
}

.cnmixed
{
    margin-left: 4px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

/*  "Occupy more rooms" is a number, so its label sits BEFORE the control
    like every other field on these screens rather than after it like a
    tick box. */
/*  THE LABEL AND ITS CONTROL BELONG TOGETHER - 13 Sep 2026.

    Mišo: "ovdje je poravnavanje malo cudno... field daleko od labele - malo
    drugacije rasporedi".

    WHY IT DRIFTED, and it is inherited rather than written. `.cnbox span` is
    `flex: 1 1 auto`, which is exactly right for the SIX rows around this one:
    they are checkboxes, and a label that fills the row is what makes the whole
    row clickable and the tick target something bigger than fifteen pixels.

    This row is not a checkbox. The same rule makes the label eat the whole
    grid cell - `minmax(260px, 1fr)`, so 260 pixels and more - and the select
    lands hard against the far edge, a hand's width from the words it belongs
    to. Two things that read as one fact, drawn as far apart as the cell
    allows.

    `flex: 0 1 auto` on the label alone. It takes the width of its words, the
    select sits next to it with the row's own 8px gap, and the pair reads as a
    pair. The label may still shrink on a narrow cell, which is what `1` in the
    middle is for - it wraps rather than pushing the select out of view.

    THE ROW STILL FILLS THE CELL, deliberately: the hover ground that marks
    which rule you are pointing at is the row's, and shrink-wrapping the whole
    label would light up only half of it. */
body.admin .cnnum
{
    align-items: center;
    gap: 8px;
}

body.admin .cnnum > span
{
    flex: 0 1 auto;
}

body.admin .cnnum select
{
    width: auto;
    min-width: 4.5em;
}

body.admin .cnactions
{
    display: flex;
    gap: 8px;
    align-items: center;
}

/*  The icon that opens it, on the execution type's heading. Beside the plus
    and quieter than it: adding a line is the common act on that stripe and
    this is the occasional one. */
body.admin .planpart-cn
{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: var(--radius-s, 6px);
    color: var(--ink-soft);
    text-decoration: none;
}

body.admin .planpart-cn:hover,
body.admin .planpart-cn.is-on
{
    color: var(--ink);
    background: var(--surface);
}

/*  OPEN IS A STATE THE ICON SHOWS. Both icons toggle - clicking an open one
    closes it - so the reader has to be able to see which one they are looking
    at the panel for, especially on a table of twenty lines. */
body.admin .lineicon.is-on
{
    border-color: var(--accent, currentColor);
    color: var(--accent, inherit);
}


/* ====================================================================== */
/*  THE DAY OVERVIEWS - 11 Sep 2026                                       */
/*                                                                        */
/*  One row per lecturer or per room, one day across, the school's own     */
/*  time ladder as the columns. Mišo: "ovi pregledi su skroz kompaktni jer */
/*  je njihova namjena pogled brzi na cijelu školu".                       */
/*                                                                        */
/*  SO EVERY RULE HERE IS ABOUT DENSITY, and the two that matter most:     */
/*                                                                        */
/*    the left column does not scroll away. A hundred rooms is wider than  */
/*    a laptop once the ladder is drawn, and a room name that scrolls off  */
/*    leaves a grid nobody can read the middle of.                         */
/*                                                                        */
/*    a cell never wraps. The moment one row is two lines tall, reading    */
/*    DOWN a column - which is the whole point, "who is free at eleven" -  */
/*    stops working, because the columns no longer line up by eye.         */
/* ====================================================================== */

.ovwrap
{
    /*  Wider than the editing screens. Those are forms and a form has a
        comfortable measure; this is a table that wants every pixel. */
    max-width: none;
}

.ovdays
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem;
    margin: 0 0 .75rem;
}

/*  THE DAY BUTTON CARRIES ITS DATE UNDER ITS NAME - 12 Sep 2026.

    Side by side the two made a strip of seven buttons wider than the grid
    they sit over, on the one screen whose whole argument is density. Stacked,
    the strip is a button and a half tall and the date is out of the way until
    somebody looks for it.

    `line-height: 1.1` on both lines rather than padding: the buttons share
    their height rule with every other .btn-small on the site, and a taller
    line would have pushed this row out of line with the week arrows beside
    it. */
.ovday
{
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    line-height: 1.1;
}

.ovdayname
{
    /*  The day is the control; the date is the label on it. */
    font-weight: 600;
}

.ovdaydate
{
    font-size: .72em;
    opacity: .75;

    /*  NEVER WRAPPED AND NEVER HYPHENATED. "14 Sep" broken over two lines
        would make one button taller than its neighbours and the whole strip
        would step. */
    white-space: nowrap;
}

.ovweek
{
    display: inline-flex;
    align-items: center;
    gap: .35rem;

    /*  The week sits apart from the days: they are two different questions
        and a single row of eleven identical buttons reads as one. */
    margin-inline-start: 1rem;
}

/* ------------------------------------------------------------------ */
/*  THE BOOKING TYPE, IN ITS OWN COLOUR - 12 Sep 2026                  */
/* ------------------------------------------------------------------ */

/*  The dot and the box are one control, so they sit in one box and the dot
    keeps its size when the bar wraps. */
.typepick
{
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

/*  THE HOUR IN A BOOKING ROW IS A LINK - 12 Sep 2026. Quiet, like the room
    names on the day overviews: a list of a hundred rows with the first column
    in link blue is a column that shouts at a reader who is scanning the
    OTHER columns. It is the pointer and the underline on hover that say so. */
.booklink
{
    color: inherit;
    text-decoration: none;
    display: block;
}

.booklink:hover
{
    text-decoration: underline;
}

.typedot
{
    /*  A CIRCLE, and a small one: it is a mark on a control, not a swatch in
        a legend. 10px is the size the palette pop-over uses for the same job
        - one dot idiom on the screen, not two. */
    inline-size: 10px;
    block-size: 10px;
    flex: 0 0 auto;
    border-radius: 50%;

    /*  A HAIRLINE ROUND IT. Several of the desktop's colours are nearly white
        and several are nearly black; without an edge one of them disappears
        into the page in one theme or the other. `currentColor` at a low alpha
        is dark on light and light on dark without naming either. */
    box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
}

/*  The week's dates, beside its number and quieter than it: the number is
    what the arrows move, the dates are what the reader checks it against. */
.ovspan
{
    font-size: .85em;
    font-weight: 400;
    opacity: .7;
    white-space: nowrap;
    margin-inline-start: .35em;
}

/*  The jump box, beside the week arrows and never on its own line: it is the
    third way of saying the same thing the arrows and the number say. Narrow
    on purpose - it takes "37" or "14.9.2026" and nothing longer. */
.ovjump
{
    display: inline-flex;
    align-items: center;
    margin-inline-start: .35rem;
}

.ovswap
{
    /*  The other axis, pushed to the far end - it leaves this screen, and a
        control that leaves belongs at the edge rather than among the ones
        that stay. */
    margin-inline-start: auto;
}

/*
 * THE FILTER OVER THE DAY OVERVIEW - 12 Sep 2026, from the second ergonomics
 * audit. "Is A-101 free at eleven?" used to be answered by scrolling a
 * hundred rows.
 *
 * NOT DRAWN UNTIL THE SCRIPT HAS WIRED IT, the same bargain `.pickset-bar`
 * makes twelve hundred lines above and for the same reason: a search box that
 * does not search is worse than no search box. wireFilter puts `is-live` on
 * the box it has wired, and only then does this appear.
 *
 * `[data-filter]` is a bare wrapper with no look of its own - it exists so
 * the wiring can find the bar and the tbody in one element - so the class
 * hangs off the element wireFilter marked, and the bar is styled through it.
 */
[data-filter] > .ovfilter
{
    display: none;
}

[data-filter].is-live > .ovfilter
{
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}

.ovfilter-search
{
    /*  The same 320px as the lists' own filter. Wide enough for a surname and
        a room code; a box the width of the page would read as a search of the
        whole school rather than of the rows underneath it. */
    flex: 0 1 320px;
    min-width: 0;
}

/*  The "nothing matched" line only ever appears with the filter, so it hides
    with it too - otherwise a browser with no script would carry a permanently
    hidden paragraph that `hidden` alone already handles, and a browser WITH
    script would show it under a bar that is not there if the wiring failed
    half way. */
[data-filter]:not(.is-live) > .ovfilter-none
{
    display: none;
}

.ovscroll
{
    overflow-x: auto;

    /*  AND A HEIGHT, WHICH IS WHAT MAKES THE HEADER FREEZE - 12 Sep 2026.
        Mišo: "mislim da obično imaju jako mnogo tih pa bi zamrznuo gornji dio
        do sati kad skrolas kod oba pogleda".

        The hour ruler has carried `position: sticky; top: 0` since the screen
        was built, and it never once stuck. Sticky is measured against the
        NEAREST SCROLLING ANCESTOR, and that is this box: `overflow-x: auto`
        makes it a scroll container on both axes (CSS resolves the other axis
        from `visible` to `auto`), so the ruler was pinned to a box that had
        no height limit, could not scroll vertically, and therefore never
        moved. The PAGE scrolled instead, and the ruler went up with it.

        One max-height fixes it and changes nothing else - the sticky rules
        below were already correct and are untouched.

        THE NUMBER HERE IS ONLY THE FALLBACK, and it was the whole answer for
        one day - 13 Sep 2026. `calc(100vh - 15rem)` is a guess at how much
        page sits above this box, and it was wrong: measured live, the box
        starts 331px down while the rule resolved to 480px in a 720px window,
        so the box ended below the fold, the PAGE scrolled, and the ruler rode
        up with it. Mišo: "kod view by lecturer i rooms nisu zamrznuti gornji
        dio do sata".

        It was wrong the moment the filter bar was added above it, and it
        would be wrong again for a school whose day buttons wrap, or whose
        calendar draws the "no dates" line. So the real height is measured -
        `data-fitbox` on the element and wireFitBox in admin.js - and this
        number is what a browser with no script gets. 24rem rather than 15rem
        because the fallback should err SHORT: a box that ends above the fold
        scrolls itself, which is the behaviour being paid for.

        min-height so a short window does not reduce the table to two rows and
        a scrollbar - below that it is better to let the page scroll. */
    max-height: calc(100vh - 24rem);
    min-height: 18rem;

    /*  A visible edge, so a table wider than the box looks scrollable rather
        than cut off. */
    border: 1px solid var(--line, #d9dce1);
    border-radius: 6px;
}

.ovgrid
{
    border-collapse: collapse;
    width: max-content;
    min-width: 100%;
    font-size: 12px;
    line-height: 1.25;
}

.ovgrid th,
.ovgrid td
{
    border: 1px solid var(--line, #d9dce1);
    padding: 1px 4px;

    /*  NEVER WRAPS. See the note at the top: one tall row and the columns
        stop lining up, which is the one thing this screen cannot afford. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ovgrid th.ovwho
{
    position: sticky;
    inset-inline-start: 0;
    z-index: 2;

    /*  Opaque, or the cells scrolling under it show through. */
    background: var(--bg, #fff);

    text-align: start;
    font-weight: 600;

    /*  Wide enough for "doc. dr. Marija Kovačević" and no wider: past this
        the names are the screen and the day is a sliver. */
    max-width: 16rem;
    min-width: 9rem;
}

.ovgrid thead th
{
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--bg, #fff);
    font-weight: 600;
    text-align: center;
}

/*  THE CORNER IS STUCK IN BOTH DIRECTIONS AT ONCE, so it needs to sit above
    both of the things that pass under it - 12 Sep 2026, with the max-height
    above, which is the first time either of them actually moved.

    It inherits `top` from the rule above and `inset-inline-start` from the
    ovwho rule, so it pins correctly without a third `position`. What it does
    NOT inherit is the right stacking order: `.ovgrid th.ovwho` carries two
    classes to `.ovgrid thead th`'s one, so the more specific rule wins and
    the corner would take z-index 2 - underneath the hour ruler sliding left
    beneath it. 4 puts it above both. */
.ovgrid thead th.ovwho
{
    z-index: 4;
}

.ovgrid th.ovhour
{
    /*  Every hour the same width, whatever its label says. Columns of
        different widths are columns that cannot be compared by eye, which is
        the only way this table is ever read. */
    width: 4.2rem;
    min-width: 4.2rem;
    font-weight: 500;
    opacity: .85;
}

/*  THE NAME IS A LINK TO THAT ENTITY'S WHOLE WEEK - 12 Sep 2026. It looks
    like the text it replaced until the pointer is on it: a column of eighty
    underlined blue names would be a wall, and the cells beside them are links
    already without announcing it. */
.ovwho a
{
    color: inherit;
    text-decoration: none;
    display: block;

    /*  The ellipsis lives on the cell; the anchor must not clip it a second
        time or a long name loses its last letters twice over. */
    overflow: hidden;
    text-overflow: ellipsis;
}

.ovwho a:hover
{
    text-decoration: underline;
}

/*  THE COLOUR LIVES ON THE EDGES - 13 Sep 2026.

    Mišo: "da li je moguće napraviti da boja backgrounda nekako fade-a od
    vanjskih rubova u tamniju boju prema sredini - cilj je da nije sve ovako
    'jarko' obojeno nego vise rubovi ali ne samo okvir".

    WHAT WAS WRONG WITH IT. The cell was the execution type's colour at full
    strength across its whole area, and a week of them is eighty flat
    rectangles of saturated orange, yellow and green. The colour is doing a
    real job - it is the legend, and it is how a shape is found at a glance -
    but at that size it stops being a mark and becomes the screen.

    A RADIAL FADE, NOT A BORDER. He asked for it in those words - "ali ne samo
    okvir" - and he is right about why: a frame draws a line AROUND a flat
    field, so the field is still flat and there is now a line as well. A fade
    keeps the hue where the eye picks it up - at the edges, where one cell
    meets the next - and gives it back the middle, which is where the words
    are.

    THREE VARIANTS WERE RENDERED AND HE CHOSE THIS ONE. The other two kept the
    dark text: the middle deepened within the same hue, and the middle washed
    pale. This is the one that actually quietens the grid, and it is the only
    one that changes the ink.

    AND THE INK HAD TO CHANGE WITH IT. #10131a was right for the whole life of
    this cell - the comment it replaces says so, and says why: the desktop's
    type colours are pale. It is wrong the moment the middle of the cell is
    the board: near-black on near-black. The two are one decision and are
    written as one rule, which is the lesson _consolidated.php already learned
    the hard way ("the ink comes with the tint, and that is the whole fix").

    THE FADE COLOUR IS A TOKEN, so this is not a dark-theme trick. --bg is
    what the sticky name column and the header row already paint themselves
    with - it IS the board, in either scheme - and on a light theme the middle
    goes to white while the ink goes... also near-white, which would be
    invisible. So the ink is stated per scheme below rather than once here.

    `color-mix` with `transparent` rather than an rgba() literal: the gradient
    has to fade to nothing at the rim, and a hard-coded rgb(20 23 29 / 0) is
    the dark theme's board written into the stylesheet by hand - the exact
    thing --bg exists to avoid.

    AND THE RIM IS LIFTED, which is his second message: "moze cak i malo
    svijetlija boja ... na rubovima". Pulling the board's dark out of the rim
    is not the same as making the rim brighter - the edge only ever gets back
    to the raw colour, which on a dark board still reads as the duller half of
    what it was. A second layer ON TOP of the fade, zero across the middle and
    a little white at the very edge, lifts the hue exactly where the eye reads
    it, and touches nothing where the words are.

    TWO TOKENS AND ONE DECLARATION, rather than two gradients written out per
    scheme. --ovfade is the colour the middle goes to and --ovlift is what the
    rim gets; the light-board block below swaps both, and on a light board the
    lift is switched OFF rather than inverted, because a white rim on a cell
    whose middle is already going white is a cell with no edge at all. */
.ovcell
{
    --ovfade: var(--bg, #14171d);
    --ovlift: rgb(255 255 255 / .34);

    color: #eef1f6;
    text-align: center;

    background-image:
        radial-gradient(115% 190% at 50% 50%,
            transparent 44%,
            var(--ovlift) 100%),
        radial-gradient(115% 190% at 50% 50%,
            color-mix(in srgb, var(--ovfade) 90%, transparent) 0%,
            color-mix(in srgb, var(--ovfade) 70%, transparent) 36%,
            color-mix(in srgb, var(--ovfade) 14%, transparent) 78%,
            transparent 100%);
}

/*  ON A LIGHT BOARD THE MIDDLE IS PALE, so the ink goes back to what it was
    and the rim lift is turned off. Same gradient, same geometry, opposite end
    of the scale - the rule above fades towards --bg whatever --bg happens to
    be, and only these two values have to be told which way round it is.

    `prefers-color-scheme` AND the explicit light class, because this
    application has both: a reader who has chosen a light theme carries
    body.mode-light, and a reader who has chosen nothing is on whatever their
    system says. Missing either one leaves white text on a white cell for
    exactly the readers who never touched a setting. */
body.mode-light .ovcell
{
    --ovlift: transparent;

    color: #10131a;
}

@media (prefers-color-scheme: light)
{
    body:not(.mode-dark) .ovcell
    {
        --ovlift: transparent;

        color: #10131a;
    }
}

.ovcell a
{
    color: inherit;
    text-decoration: none;
    display: block;
}

.ovcell a:hover
{
    text-decoration: underline;
}

.ovgap
{
    /*  A free hour is not empty space, it is the ANSWER - so it is drawn as a
        cell with a faint ground rather than as nothing at all. */
    background: color-mix(in srgb, currentColor 4%, transparent);
}

.ovfree th.ovwho
{
    /*  A row with nothing in it all day. Dimmed, never hidden: "which room is
        free on Thursday" is answered by exactly these rows. */
    opacity: .55;
    font-weight: 500;
}

.ovclash
{
    /*  Two lessons at one hour in one room. It happens - deliberate overlaps
        are a thing planners do - and it is worth seeing rather than losing
        the second one behind the first. */
    outline: 2px solid var(--danger, #b4232a);
    outline-offset: -2px;
}

.ovmore
{
    font-size: 10px;
    font-weight: 700;
    margin-inline-start: 2px;
    opacity: .8;
}

/*  DARK MODE. The sticky cells need an opaque ground of their own or the rows
    show through them; everything else inherits. */
body.mode-dark .ovgrid th.ovwho,
body.mode-dark .ovgrid thead th
{
    background: var(--bg, #171a21);
}

@media (prefers-color-scheme: dark)
{
    body.admin:not(.mode-light) .ovgrid th.ovwho,
    body.admin:not(.mode-light) .ovgrid thead th
    {
        background: var(--bg, #171a21);
    }
}

/* ====================================================================== */
/*  APPROVING BOOKINGS - 12 Sep 2026                                      */
/*                                                                        */
/*  Mišo: "kako vidimo mnogo skola upotrebljava cijeli wtt sistem samo za  */
/*  booking/approval sistem - i nas je fino napravljen - zato bi kopirao   */
/*  svaku funkciju iz desktop variante".                                   */
/*                                                                        */
/*  The desktop's Approve-reservations panel puts its buttons in a row     */
/*  under the list, with the two harmless ones on the left and the two     */
/*  that decide something on the right. This is that, in one line.         */
/*  Everything here is scoped to the bookings list; nothing in it can      */
/*  reach a school that does not approve, because the markup is not drawn  */
/*  there at all.                                                         */
/* ====================================================================== */

.listtable th.pickcol,
.listtable td.pickcol
{
    /*  Just wide enough for the box and its focus ring. Narrower and the
        ring is clipped by the next column. */
    width: 28px;
    padding-inline: 8px 0;
    text-align: center;
}

.listtable td.pickcol
{
    /*  The row can be three lines tall - dates, hours, a list of rooms - and
        a box centred in that is a box floating beside nothing. It belongs
        level with the first line, which is where the eye starts. */
    vertical-align: top;
    padding-block-start: 10px;
}

.pickbox
{
    width: 15px;
    height: 15px;
    margin: 0;
    cursor: pointer;

    /*  --brand, not --accent. There is no --accent in this application and
        there never was; the first version of this block invented three token
        names (--accent, --panel, --topbar-bg), every one of them fell through
        to its hard-coded fallback, and the result was a light card sitting on
        a dark page. The palette is at the top of app.css: --brand,
        --brand-ink, --bg, --surface, --surface-2, --ink, --ink-soft, --line,
        --line-strong, --danger, --ok. Nothing else exists. */
    accent-color: var(--brand);
}

.approvebar
{
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-block: 10px 4px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: 8px;

    /*  A ground of its own, slightly off the page. The bar is a place where
        something happens, and the list above it is not. --surface-2 is the
        palette's word for that and it flips with the scheme by itself, which
        is why the hand-written dark override this block used to carry is
        gone rather than corrected. */
    background: var(--surface-2);

    /*  AND IT FOLLOWS THE READER DOWN THE LIST - 12 Sep 2026.
        
        The bar sits under the table and the tick boxes are in the rows, so at
        a hundred rows a page the route for one decision was: tick a booking
        near the top, scroll the whole page to reach the buttons, press, and
        be returned to the top for the next one. This is the screen Mišo says
        whole schools use the product for - "mnogo skola upotrebljava cijeli
        wtt sistem samo za booking/approval sistem" - so that scroll is paid
        every morning at every one of them.
        
        Sticky to the BOTTOM, which is where it already is in the document:
        the bar does not move, the rows move under it, and the tick box and
        the button are on screen together for the first time. The form wraps
        both, so nothing about the submit changes.
        
        z-index 5 clears the row hover and the type dots; it stays under the
        left rail, which is 20. The bar already carries its own ground and a
        border, so it reads correctly floating over the rows rather than
        needing a shadow. */
    position: sticky;
    bottom: 0;
    z-index: 5;
}

/*
 * THE WHEN COLUMN: DATE AND HOUR ON ONE LINE, AND THE ROWS CLOSER TOGETHER
 * - 13 Sep 2026. Mišo: "stavi datum, sat u istoj liniji - ne trosim vertical
 * space gdje je potreban za mnogo bookinga - i između 2 bookinga malo stisni".
 *
 * `.sub` is a block everywhere else in the application and is right to be:
 * under a heading it is a second line. In this cell it was costing a line on
 * every row of a list that runs to hundreds, to say something that fits
 * beside the date with room to spare.
 *
 * INLINE-BLOCK AND NOT INLINE, so the pair can still WRAP as a unit when the
 * column is narrow - "2026-06-30 – 2027-06-23 10:00–11:00" is long, and a
 * narrow When column should break it rather than push the table sideways.
 * One line where it fits, two where it does not.
 */
.bookwhen .sub
{
    display: inline-block;
    margin-inline-start: .5em;
}

/*
 * AND THE TYPE COLUMN, FOR THE SAME REASON - 13 Sep 2026. Mišo, looking at a
 * row reading "Other" over "Available": "zašto ovaj booking uzme 2 linije?"
 *
 * The second line is the booking's own description, and most of them are a
 * word or two. Beside the type it costs nothing; under it, it costs a line on
 * every row of a list that runs to hundreds - and the row's height is then set
 * by the tallest cell, so ONE two-line cell makes the whole table twice as
 * tall.
 *
 * INLINE-BLOCK, again, so a long description still breaks to its own line
 * rather than widening the column: short ones sit beside the type, long ones
 * wrap as a unit. Nothing is lost in the bad case and a line is saved in the
 * common one.
 *
 * NOT APPLIED TO THE THIRD COLUMN, deliberately. Rooms, lecturers and groups
 * are three `.sub` runs that mean three different things, and the template's
 * own note says why they are stacked: "a booking of one room and four groups
 * is unreadable as one run-on sentence". Density is not worth that.
 */
.booktype .sub
{
    display: inline-block;
    margin-inline-start: .5em;
}

/*  AND THE ROWS TIGHTER. The When cell was two lines, so the row's padding
    was set for a two-line cell; with one line that padding is a third of the
    row's height doing nothing. Scoped to this list - `.bookwhen` is only on
    the bookings table - so no other list moves. */
.listtable tr:has(> .bookwhen) > td,
.listtable tr:has(> .bookwhen) > th
{
    padding-block: 4px;
}

.approvebar-what
{
    font-weight: 600;
    margin-inline-end: 2px;
}

.approvebar-gap
{
    /*  Pushes the two deciding buttons to the far end, away from Select all -
        the same separation the desktop panel gets from sheer width. A mis-hit
        on a crowded bar is how somebody denies what they meant to confirm. */
    flex: 1 1 auto;
    min-width: 12px;
}

.approvebar-what,
.approvebar-clear,
.approvehint
{
    /*  The bar carries its own ground, so the text on it takes the page's ink
        rather than whatever a light-on-dark fallback would have given it. */
    color: var(--ink);
}

.approvebar-clear
{
    margin-block: 10px 4px;
    opacity: .75;
}

/*  WHAT THE LETTER SAYS - the <details> under the deciding bar.
 *
 *  FOLDED AWAY BY DEFAULT, and that is the whole reason it is a <details> and
 *  not a panel: the common act is tick, press, done, and a planner who never
 *  opens this gets the plain letter the desktop sends when its box is empty.
 *  Putting a textarea permanently under the bar would make every approval look
 *  like it wanted something written.
 *
 *  IT BORROWS THE BAR'S SHAPE rather than inventing one - same border, same
 *  radius, same ground - so the two read as one control that happens to have a
 *  second half. Only tokens that exist are used here: --line, --surface-2 and
 *  --ink, the same three the bar above takes. An invented custom property
 *  falls through to its hard-coded fallback and produces a light card on a
 *  dark page, which is exactly what happened the first time this bar was
 *  written. */
.approvemail
{
    margin-block: 0 10px;
    padding: 0 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface-2);
    color: var(--ink);
}

.approvemail > summary
{
    cursor: pointer;
    padding-block: 10px;
    font-weight: 600;
}

/*  Breathing room only once it is open, so the folded state stays a single
    quiet line. */
.approvemail[open]
{
    padding-block: 0 12px;
}

.approvehint
{
    margin-block: 10px 4px;
}

.approvehint a
{
    font-weight: 600;
}

.btn-danger
{
    /*  THE ONLY DESTRUCTIVE BUTTON ON THIS SCREEN, and it says so twice - in
        its label and in the question the browser asks. Outline rather than a
        filled red: a solid red block beside a solid blue one is two buttons
        competing to be pressed, and the one that deletes should never be the
        one the eye lands on first. */
    color: var(--danger);
    border-color: var(--danger);
    background: transparent;
}

.btn-danger:hover,
.btn-danger:focus-visible
{
    color: #fff;
    background: var(--danger);
}

/*  THE APPROVAL BADGE IN THE TOP BAR - 12 Sep 2026.

    Mišo: "treba vidi uvijek i koliko je approvala - po moje odmah kad se
    logiras negdje gore pored neke ikone - koju onda kliknes i ides direktno
    na bookinge sa filtriranim not approved".

    IT IS NOT A .topbar-icon, and the first version of it was. That class is
    `width: var(--control-h); padding: 0` - a fixed SQUARE, because every
    other control on this bar holds one glyph and nothing else. A number
    pinned to its corner had nowhere to go: the render came out with the
    stamp clipped on one side and "13" hanging off the other.

    So this is its own shape - the same height and border as its neighbours,
    but as wide as what is in it, with the icon and the number side by side
    inside. Nothing overflows, nothing is positioned absolutely, and three
    digits widen it instead of spilling out of it.

    IT DOES NOT BLINK. The desktop alternates two pictures on a timer, which
    is what a 2004 toolbar had instead of a colour. Here --brand does that
    job, and blinking chrome in a browser tab reads as an advertisement. */

.topbar-badge
{
    display: inline-flex;
    align-items: center;
    gap: 6px;

    /*  Auto width, which is the whole point - and the padding that
        .topbar-icon zeroes, put back so the pair is not against the border. */
    width: auto;
    padding-inline: 10px;

    color: var(--brand-ink);
    border-color: var(--brand);
}

.topbar-badge:hover
{
    color: var(--brand-ink);
}

.topbar-badge .badgenum
{
    min-width: 17px;
    padding-inline: 5px;

    font-size: 11px;
    font-weight: 700;
    line-height: 17px;
    text-align: center;

    color: #fff;
    background: var(--brand);
    border-radius: 9px;
}


/* ------------------------------------------------------------------ */
/* The Years row on Generate, pinned                                   */
/* ------------------------------------------------------------------ */

/*  Mišo, 13 Sep 2026, with a picture of it: "malo neporavnano". Year 1 sat a
    line higher than 2 and 3 and further left, so three one-character labels
    read as a broken layout rather than as a range.

    WHY IT DRIFTED. `.check` is declared three times in this stylesheet, and
    two of those say `align-items: flex-start` with different padding - 5px 4px
    in one place, 6px 0 in another. Whichever wins, the box is pinned to the
    TOP of a flex item whose height comes from the row, so a row that is taller
    than one item puts that item's box above the others. `.setform-inline` sets
    no align-items at all, so its children stretch and the drift has somewhere
    to happen.

    TWO CLASSES IN THE SELECTOR, DELIBERATELY. `.setform.setform-inline` beats
    every single-class `.check` rule on specificity rather than on order, so
    this cannot be undone by somebody adding a rule further up - which is how
    it drifted in the first place.

    It is scoped to this one row: `setform-inline` appears in exactly one
    template, generate.php, and nowhere else. */
.setform.setform-inline
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 16px;
}

.setform.setform-inline > .check-inline
{
    align-items: center;
    padding: 2px 0;
    margin: 0;
    gap: 7px;
}

/*  The box's own 2px nudge is for a label that wraps to two lines. A year is
    one character and never wraps, so here it is only a 2px error. */
.setform.setform-inline > .check-inline > input[type="checkbox"]
{
    margin: 0;
}


/*  A BUTTON BESIDE STACKED FIELDS SITS ON THEIR BOTTOM, NOT THEIR MIDDLE.

    Mišo, 13 Sep 2026, on the Non-teaching days tab: "ovdje poravnat".

    `.setrow` is `align-items: center`, which is right for what it was written
    for - the colour swatches and the three scheme buttons, all one line tall.
    A `.field` is not one line tall: it is a label stacked over a box, so
    centring a plain button against it lands the button in the gap between the
    two, which reads as a control belonging to neither.

    Scoped with `:has()` so the swatch row is untouched: only a row that
    actually contains a stacked field changes. Browsers without :has() keep
    what they have, which is what they have today. */
.setrow:has(> .field)
{
    align-items: flex-end;
}



/*  THE REPORT BAR IS A ROW, AND app.css ALREADY SAID SO - 13 Sep 2026.

    Miso, twice, on the reports screen: "tu samo malo poravnave", then
    "neporavnano jos uvijek" - FROM and TO stood ABOVE their date boxes while
    REPORT stood BESIDE its select, so the one line read as three different
    alignments.

    WHY THE FIRST FIX DID NOT TAKE. app.css:3315 sets

        .reportrow .field { display: inline-flex; align-items: center; }

    and that is the whole of what it sets. `flex-direction` it never names -
    so the value keeps coming from admin.css:183, `.field { flex-direction:
    column }`, which this application applies to every stacked form field on
    every other screen. Specificity was never the problem: a property that is
    not declared cannot win, and `inline-flex` merely changed the box, not the
    axis inside it.

    ONE PROPERTY IS THE WHOLE FIX, and it belongs here rather than in app.css
    for the usual reason - app.css is COPIED from the web application by
    tools/build-from-web.py and anything written into it is lost on the next
    carry. The public application never loads admin.css, has no column rule on
    .field, and therefore has always drawn this bar correctly; this rule only
    undoes an admin-only default on an admin-only screen.

    `align-items: center` is repeated on purpose: with the axis turned, it now
    means "label and box share a centre line" rather than "label centred over
    box", which is the actual thing being asked for. */
body.admin .reportrow .field
{
    flex-direction: row;
    align-items: center;
    gap: 8px;
}

/*  And the labels stop shouting in two sizes. app.css gives the report bar's
    own labels 11.5px/650; admin.css:190 gives every .field-label 11px/600
    with .08em of tracking. Both were landing on this bar - the ones inside a
    .field from admin, the two loose ones from app - so FROM sat a half pixel
    smaller and a hair wider than REPORT beside it. One declaration for all of
    them, taken from the bar's own rule, which is the one written for this
    line. */
body.admin .reportrow .field-label
{
    font-size: 11.5px;
    font-weight: 650;
    letter-spacing: .03em;
    white-space: nowrap;
}



/* ======================================================================
   THE GENERATE SCREEN, IN SECTIONS - 13 Sep 2026.

   Mišo: "i kod generate lijepo razdijeli u sekcije".

   WHAT IT WAS. Four groups - the run, the programmes, the years, the two
   limits - one under another down a single 560px column, separated by nothing
   but a hairline over a tiny uppercase heading. That reads as one long form
   with subtitles in it, and the eye cannot tell where "what the run does" ends
   and "what the run is allowed to touch" begins. The programmes list makes it
   worse the more programmes a school has: eleven rows push the two limits a
   screenful below the button they belong to.

   NOTHING NEW IS INVENTED HERE. `.setgrid` / `.setcard` / `.setcard-wide`
   already exist, written for the settings screen on 7 Sep, and the comment
   over them says in as many words that they carry no Generate-specific rule
   so that the next long settings screen can become a matrix by wrapping its
   groups in the same two elements. This is that screen, and these four rules
   are only the adjustments a card needs that a page does not.
   ====================================================================== */

/*  TWO COLUMNS, NOT AS MANY AS FIT.

    `.setgrid` asks for minmax(320px, 1fr), which on a wide monitor resolves to
    three or four columns. That is right for a wall of switches and wrong here:
    there are three cards, one of them is a list of programme names and one
    holds a free-text filter, and neither reads well in a 320px slot. 420px
    lands on two columns at every width this application is used at, and
    `min()` keeps it from overflowing a phone, where auto-fit then gives one. */
.gengrid
{
    grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
}

/*  A HEADING INSIDE A CARD DRAWS NO LINE OF ITS OWN.

    `.setgroup-apart` is a margin, a padding and a border-top - the way a page
    separates one group from the next when there is nothing else to do the job.
    Inside a card the card's own edge has already done it, and the rule would
    print a second line 20px below the first. `.setcard > .setgroup` next door
    already fixes the margins; this fixes the line, and the two are separate
    rules because the margins apply to every heading in a card and this applies
    only to the ones carrying the apart class. */
.setcard > .setgroup-apart
{
    padding-top: 0;
    border-top: 0;
}

/*  A card is already narrow - the grid decided its width - so the 560px cap
    that keeps a form readable across a full page here only leaves the right
    half of the card empty. Same reasoning, and the same shape, as the
    .field-narrow rule above. */
.setcard .setform
{
    max-width: none;
}

/*  THE PROGRAMME TICKS, ACROSS RATHER THAN DOWN.

    The programmes card spans both columns (it is the one list where the names
    are long and the school may have twenty), so down one column it would be
    twenty rows tall inside a box twice as wide as it needs. As a grid it is
    ten rows in two columns, or four in five on a wide monitor, and the card
    stops being the tallest thing on the screen by a factor of three.

    `.setform > * + *`'s 12px is turned off and replaced by the grid's own
    row-gap. The margin is applied to every child except the FIRST IN DOCUMENT
    ORDER, which in two columns is the first child of column one only - so the
    top of column two sat 12px lower than the top of column one, which is
    precisely the misalignment this screen was being tidied to remove. */
.setform-cols
{
    max-width: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
    gap: 0 26px;
    align-items: start;
}

.setform-cols > * + *
{
    margin-block-start: 0;
}



/*  TWO STACKS OF CARDS, NOT A GRID OF THEM - 13 Sep 2026.

    Mišo, on the Generate settings tab: "ovi rules su bitni zato bi ih dao gora
    na vrh desno, u drugu kolonu".

    WHY .setgrid COULD NOT JUST BE REORDERED. A CSS grid gives every cell in a
    row the height of the tallest item in that row. The rules card is fifteen
    switches and the working-hours card is three fields, so putting the two
    side by side in row 1 leaves half a screen of nothing under the shorter
    one. Masonry is the layout that solves this and is not something to ship a
    settings page on today.

    So the columns are containers, not cells: each .setstack packs its own
    cards top to bottom and neither knows how tall the other is. The template
    decides what goes in which, which is also what makes "rules first, on the
    right" sayable at all.

    THE SAME `auto-fit` AS .setgrid, so a narrow screen gets one column and the
    two stacks read one after the other in DOM order. 420px rather than
    .setgrid's 320px because a stack holds whole cards rather than one card's
    worth of content: at 320 the number fields inside them start wrapping. */
.setgrid-stacks
{
    grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
    align-items: start;
}

.setstack
{
    display: flex;
    flex-direction: column;
    gap: 18px;

    /*  Without it a stack refuses to shrink below its widest card's content
        and the two columns overflow the page rather than sharing it - the
        same flex/grid floor that .setcard already sets on itself. */
    min-width: 0;
}



/*  THE ROW'S ICONS END WHERE THE HEADING'S DO - 13 Sep 2026.

    Mišo: "i da je poravanano desno sa gornjom ikonom".

    MEASURED, and it was twelve pixels. `.listtable td` carries
    `padding: 10px 12px 10px 0`, so every cell - the last one included - keeps
    12px of air on its right. `.planpart-head` is a flex row filling the same
    .planpart, and its last item, `.planpart-cn`, sits flush against that edge
    with nothing after it. Two right edges twelve pixels apart, one directly
    above the other, on the one axis the eye checks hardest.

    The padding is dropped on THIS cell only. Taking it off `.listtable td`
    would move the last column of every table in the administration, and the
    12px is right everywhere it is not sitting under a heading's own control.

    `.num` already does exactly this, for exactly this reason, three hundred
    lines up: `padding-right: 0` so a column of figures ends on the table's
    edge rather than near it. */
body.admin .lineact
{
    padding-right: 0;
}

/*  ...and the last icon gives back its own left-hand gap on the right. The
    icons are spaced with `margin-left: 4px`, which is correct between them and
    contributes nothing at the end - but a `.btn` brings its own horizontal
    padding, and the SVG inside a 30px box is centred, so the visual edge of
    the glyph is a few pixels short of the box. The box is what has to line up
    with the heading's box, not the glyph with the glyph - both are centred in
    their own square, so squaring the boxes squares the marks. */
body.admin .lineact .lineicon:last-child
{
    margin-right: 0;
}

/*  THE ONE THAT LEAVES. Same size and same quiet as its two neighbours - it is
    not more important than them - but it takes the brand colour on hover
    rather than plain ink, because that is what every other link out of a
    screen in this application does, and a reader should be able to tell "this
    opens something here" from "this takes me somewhere" before pressing it. */
body.admin .lineicon-goto:hover,
body.admin .lineicon-goto:focus-visible
{
    color: var(--brand);
}

/*  THE BIN, AND WHY IT IS QUIET UNTIL AIMED AT - 13 Sep 2026.

    Mišo asked for it "crvenkast". It is, under the pointer. A permanently red
    icon on every row of a twenty-line plan makes the whole table look like a
    list of problems, which is the misreading the overlap shade produced on
    this same day and the reason `.lineicon-danger` above already works this
    way. The colour belongs to the ACT, so it appears where the act is about
    to happen.

    `is-on` is the state where its question is open below, and there it stays
    red: the row and the strip under it have to read as one thing. */
body.admin .lineicon-bin:hover,
body.admin .lineicon-bin:focus-visible,
body.admin .lineicon-bin.is-on
{
    color: var(--danger);
    border-color: var(--danger);
}

/* ==================================================================== */
/*  "Delete this line's lessons?" - the strip under a shut row           */
/* ==================================================================== */

/*  IT HANGS OFF THE ROW ABOVE IT, so it must not carry the `line-foot`
    padding, which assumes an OPEN row above and none of its own space. This
    one follows a shut row that has already drawn its bottom rule, so it needs
    room of its own. */
.planlines .line-ask > td
{
	padding-top: 8px;
	padding-bottom: 8px;
	background: color-mix(in srgb, var(--danger) 7%, transparent);
	border-bottom: 1px solid var(--line);
}

/*  One line: the question, then the two answers, and they stay on one line
    until the table is genuinely narrow. `wrap` rather than `nowrap` because a
    long translation must not push the buttons off the edge of a table that
    already scrolls sideways. */
.lineask
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
}

/*  The question takes the space, so the buttons sit together at the right
    where a pair of answers belongs. */
.lineasktext
{
	flex: 1 1 auto;
	min-width: 0;
	font-weight: 600;
}



/* ====================================================================== */
/*  A SET OF TICKS IN THE LIST BAR - 13 Sep 2026.                         */
/*                                                                        */
/*  Mišo, on the rooms list: "gore filter field mozes malo skratit, a iza  */
/*  dodat jos jedan filter ne string ne kombo neki izbor po room equipment */
/*  koji pokaze samo sobe sa tom opremom - moze i po checkboxes da mozes   */
/*  vise equipments odjednom oznacit - jako korisno kod mnogo soba".       */
/* ====================================================================== */

/*  THE SEARCH BOX GIVES BACK THE ROOM THE NEW CONTROL NEEDS.

    It was `flex: 1` - it took every pixel the bar had left, which was right
    while it was the only thing in the middle of it. `max-width` rather than a
    smaller flex: it still grows on a narrow screen, where there is nothing to
    share the bar with, and simply stops at a width that is already more than
    anybody types into a filter. */
body.admin .listtop .listbar-search
{
    max-width: 26rem;
}

/*  CLOSED IT IS ONE WORD. The summary carries the marker, the label and - only
    while the filter is on - how many boxes are ticked, so a reader who has
    scrolled past the bar can still see that the list in front of them is
    narrowed. `list-style: none` plus the ::marker rule kills the browser's own
    triangle in both engines; the caret is drawn below so it can be positioned
    and coloured with the rest of the bar. */
body.admin .tickset
{
    position: relative;
}

body.admin .tickset-head
{
    display: inline-flex;
    align-items: center;
    gap: 6px;

    height: 32px;
    padding: 0 12px;

    border: 1px solid var(--line-strong);
    border-radius: var(--radius-m, 8px);
    background: var(--surface);

    font-size: 12.5px;
    font-weight: 600;
    color: var(--ink-soft);
    white-space: nowrap;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

body.admin .tickset-head::-webkit-details-marker
{
    display: none;
}

body.admin .tickset-head::marker
{
    content: '';
}

/*  The caret, drawn rather than borrowed, and it turns when the panel opens -
    the one piece of feedback a disclosure owes its reader. */
body.admin .tickset-head::after
{
    content: '';
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transition: transform 120ms ease;
}

body.admin .tickset[open] > .tickset-head
{
    color: var(--ink);
    border-color: var(--brand);
}

body.admin .tickset[open] > .tickset-head::after
{
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce)
{
    body.admin .tickset-head::after
    {
        transition: none;
    }
}

/*  HOW MANY ARE TICKED, on the summary. Same shape as .editcount beside it, a
    size down: it is an annotation on a control rather than the answer to
    "how many rooms are there". */
body.admin .tickset-count
{
    display: inline-flex;
    align-items: center;
    justify-content: center;

    min-width: 18px;
    height: 18px;
    padding: 0 5px;

    border-radius: 9px;
    background: var(--brand);
    color: var(--on-brand, #fff);

    font-size: 11px;
    font-weight: 700;
}

/*  OPEN, IT IS A PANEL AND NOT MORE BAR.

    Absolute, so a faculty with fifty-eight kinds of equipment does not push
    the table down the page every time somebody opens the filter - the bar
    keeps its height whatever is inside this. It is a filter: it is read,
    ticked and shut.

    Multi-column for the same reason as the Generate switches, and capped in
    height with its own scroll so fifty-eight entries cannot run off the
    bottom of the screen. */
body.admin .tickset-body
{
    position: absolute;
    z-index: 30;
    top: calc(100% + 6px);
    inset-inline-start: 0;

    min-width: 18rem;
    max-width: min(42rem, 90vw);
    max-height: 40vh;
    overflow-y: auto;

    padding: 10px 14px 12px;

    border: 1px solid var(--line-strong);
    border-radius: var(--radius-m, 8px);
    background: var(--surface);
    box-shadow: 0 12px 28px -18px rgba(0, 0, 0, .55);

    columns: 220px 3;
    column-gap: 26px;
}

body.admin .tickset-body > .check
{
    break-inside: avoid;

    /*  .check-inline zeroes the margin for a row of them; inside a column each
        one is its own line and wants the air back. */
    display: flex;
    margin: 0;
    padding-block: 4px;
}

/*  Clearing the whole filter. It spans the columns rather than sitting at the
    foot of the last one, because it is about all of them. */
body.admin .tickset-clear
{
    column-span: all;

    display: inline-block;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--line);

    font-size: 12px;
    font-weight: 600;
    color: var(--brand);
    text-decoration: none;
}

body.admin .tickset-clear:hover
{
    text-decoration: underline;
}

/*  ON A PHONE THE PANEL IS THE WIDTH OF THE SCREEN AND ONE COLUMN. Absolute
    positioning is kept - the bar still must not grow - but it is anchored to
    the bar's own edge rather than to the summary, which at 360px would put a
    twenty-rem panel half off the side. */
@media (max-width: 560px)
{
    body.admin .tickset-body
    {
        inset-inline-start: auto;
        inset-inline-end: 0;
        min-width: 0;
        width: min(22rem, 86vw);
        columns: auto;
    }
}



/*  THE PLAN TABLES LINE UP WITH EACH OTHER - 13 Sep 2026.

    Mišo: "ovdje bi ipak bilo ljepse da kolona Weeks i ostale jesu poravnane
    jedna ispod druge".

    EACH EXECUTION TYPE HAS ITS OWN <table>, and that is the cause. A course
    with four execution types draws four `table.planlines`, one under each
    type's heading - which is right, because a section is a heading plus its
    own lines and the `+` that adds one belongs to that heading. But an `auto`
    table sizes its columns from ITS OWN content, so the four tables measure
    four different sets of rows and agree about nothing: "predavanje" with one
    lecturer gives LECTURERS a narrow column, "laboratorijske vaje" with two
    gives it a wide one, and WEEKS lands in a different place in each. Reading
    down the page then means re-finding every column at every heading.

    The room column makes it worse rather than better: where no line names a
    room the column collapses to its heading, which is a different width again.

    `table-layout: fixed` IS THE WHOLE FIX. It sizes from the first row's
    declared widths instead of from the content, so every table on the screen
    resolves identically whatever is in it - and the columns stack.

    THE WIDTHS ARE THE HEADER ROW'S, and that row has SIX cells for SEVEN
    columns: the weeks heading spans two, because "valid from" and "valid to"
    are two boxes when the line is open and one range when it is shut. The
    browser splits that 13% across the pair.

    LECTURERS IS THE ONE LEFT `auto`. Something has to absorb the remainder,
    and a list of names is the cell that genuinely varies - everything else is
    a number, a code or three icons whose width is known. */
body.admin table.planlines
{
    table-layout: fixed;
}

body.admin .planlines th:nth-child(1)   /* hours / weeks - the typed plan */
{
    width: 17%;
}

body.admin .planlines th:nth-child(2)   /* weeks - spans two columns */
{
    width: 13%;
}

body.admin .planlines th:nth-child(3)   /* room */
{
    width: 15%;
}

/*  th:nth-child(4) is LECTURERS and is deliberately not given a width. */

body.admin .planlines th:nth-child(5)   /* cohorts */
{
    width: 9%;
}

/*  THREE ICONS AND THE GAPS BETWEEN THEM. In pixels and not a percentage: the
    controls do not grow with the table, so a percentage would give them a
    puddle on a wide screen and clip the arrow on a laptop. */
body.admin .planlines th:nth-child(6)
{
    width: 118px;
}

/*  AND A FIXED TABLE STILL HAS TO LET ITS CELLS WRAP. `table-layout: fixed`
    does not shrink content - a long list of lecturers in a column narrower
    than the names would simply overflow it - so the two cells that can hold
    arbitrary text are told to break. Everything else on the row is short by
    construction. */
body.admin .planlines .linewho-shut,
body.admin .planlines td:first-child
{
    overflow-wrap: anywhere;
}



/*  THE WORD IN FRONT OF THE COURSE LEADER'S NAME - 13 Sep 2026.

    Quieter than the name and quieter than the type: it is a label, and a label
    that competes with the two names on the row would have replaced one
    ambiguity with a crowd. Uppercase and small, the same treatment every other
    field label on these screens gets, so it reads as "what this is" rather
    than as a second piece of the sentence.

    `flex: 0 0 auto` and nowrap because the heading is a single line that never
    wraps - see .planpart-head - and this is the one item on it that must not
    be the one to shrink: a label truncated to "COURSE LEA…" answers nothing.
    The two NAMES shrink instead; they already do. */
body.admin .planpart-chieflabel
{
    flex: 0 0 auto;
    margin-left: 4px;

    font-size: 10px;
    font-weight: 650;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-soft);
    white-space: nowrap;
    opacity: .85;
}



/* ====================================================================== */
/*  COVERING A LESSON, FROM THE DETAILS DRAWER - 13 Sep 2026.             */
/*                                                                        */
/*  The panel Repo\Substitutions finally has a screen for. See            */
/*  templates/_coverpanel.php for what it does and why cancelling is the  */
/*  first verb rather than the second.                                    */
/* ====================================================================== */

/*  A PANEL INSIDE A DRAWER, so it has to read as one level in rather than as
    a second drawer. A left edge in the accent and nothing else: a bordered box
    inside a bordered box is two frames for one thing, and the drawer's own
    edge is already doing that job. */
.coverpanel
{
    margin: 14px 0 4px;
    padding: 2px 0 2px 12px;
    border-left: 3px solid var(--accent, var(--brand));
}

.coverpanel > .setgroup
{
    margin-top: 2px;
    color: var(--ink);
}

/*  The fields stack, with the rhythm .setform uses elsewhere. Written here
    rather than by adding .setform to the form, because that class also caps
    the width at 560px - right on a settings page, wrong inside a drawer that
    is already narrow. */
.coverform
{
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 8px;
}

/*  A SELECT IN HERE MUST NOT PUSH THE DRAWER WIDE. The candidate options carry
    a name plus the four numbers the desktop's own table carries - "Marija
    Novak — busy · 4 h today, 18 h this week" - and a select sized to its
    widest option would be half a screen. The box takes the drawer's width and
    the browser scrolls the list. */
.coverform .input,
.coverform select
{
    width: 100%;
    max-width: 100%;
}

/*  WHO IS AWAY, WHEN IT IS ALREADY ARRANGED. Stated at full strength: it is
    the answer to the question the reader opened the lesson with, not a note
    about it. */
.coverwho
{
    margin: 0 0 6px;
    font-weight: 600;
    color: var(--ink);
}

/*  AND WHERE THE OTHER HALF LIVES. Quiet, at the foot, after the button - a
    reader who wanted the whole series has to be told where that is or they
    will use this and wonder why next week is unchanged. */
.coverhint
{
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--line);
}



/*  TWO RULES ARE A PAIR, NOT A GRID OF TWO - 13 Sep 2026.

    Mišo, on the execution type's constraint panel: "ova dva gore checkboxa su
    malo nabacana - nekako ne znam - daj liniju između njih i donjih, pa jednu
    kratku vertikalnu po sredini pa u svaku gornju celiju centriraj checkbox".

    WHY THEY LOOKED THROWN ON. They were in `.cngrid`, which is
    `repeat(auto-fit, minmax(260px, 1fr))` - correct for the SEVEN rules a turn
    line carries, and wrong for exactly two. On a wide panel auto-fit made
    columns far wider than either label, so each box hugged the left edge of a
    column with no edge of its own and the pair read as two strays at opposite
    ends of an empty band.

    THREE LINES, AND THEY ARE HIS THREE. Two equal halves with the box centred
    in each; one short rule between them, which is the second cell's own left
    border and is therefore exactly as tall as the row; and one rule under the
    pair, separating it from the room attributes below.

    A SEPARATE CLASS AND NOT A CHANGE TO .cngrid, because the seven still want
    what they have: seven boxes centred in seven wide columns would be a row
    nobody could read down. */
.cnpair
{
    display: grid;
    grid-template-columns: 1fr 1fr;

    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line);
}

/*  Centred in its half. `justify-content` and not `text-align`, because
    `.cnbox` is a flex row - the box and its label travel together and the pair
    is what gets centred, rather than the label sliding away from its box. */
.cnpair > .cnbox
{
    justify-content: center;
}

/*  THE SHORT VERTICAL. On the second cell rather than as a pseudo-element on
    the container: a border is exactly the height of the row it is on, which is
    what "kratku" asks for, and it needs no absolute positioning to stay there
    when the label wraps. */
.cnpair > .cnbox + .cnbox
{
    border-left: 1px solid var(--line);
}

/*  ONE COLUMN ON A NARROW PANEL, and the vertical rule goes with it - a line
    between two things that are now above and below each other would point the
    wrong way. */
@media (max-width: 620px)
{
    .cnpair
    {
        grid-template-columns: 1fr;
    }

    .cnpair > .cnbox
    {
        justify-content: flex-start;
    }

    .cnpair > .cnbox + .cnbox
    {
        border-left: 0;
    }
}



/*  THE DRAWER'S BUTTONS ARE ONE COLUMN, NOT TWO PARAGRAPHS - 13 Sep 2026.

    Mišo: "tu bi poravnane - oba sira gumba". They were a `<p>` each, so every
    button shrink-wrapped its own words and the pair stepped down the panel at
    two different widths, aligned on neither edge.

    `align-items: stretch` is what makes them equal: a .btn is inline-flex and
    would otherwise take the width of its label. Stretched, both take the
    column's - which is the drawer's - and the two line up top and bottom. */
.detail-do-stack
{
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
}

.detail-do-stack > .btn
{
    justify-content: center;
}

/*  The sentence under them is an aside, not a third button: it explains the
    grid rather than doing anything, so it gets the space that separates it
    from a control and none of the weight. */
.drawer-hint
{
    margin-top: 10px;
    line-height: 1.45;
}


/* ------------------------------------------------------------------ */
/* The room panel's action row - item K, 13 Sep 2026                   */
/* ------------------------------------------------------------------ */

/*  TWO BUTTONS IN ONE ROW, AND ONLY IN THESE PANELS.

    .form-actions carries a margin and nothing else - it is a block, so two
    buttons in it sit side by side with a single space between them and, in a
    drawer this narrow, the second one falls half off the edge with nothing
    holding the pair together.

    The room panel is the first place in the application with a second button
    in that row: Set room, and - only after a refusal has named what is in the
    way - Put it there anyway. Two presses that mean different things need to
    look like two things.

    SCOPED TO .coverpanel ON PURPOSE. Every editing screen in this application
    uses .form-actions, and a display change on the bare class would move the
    button on all of them to fix one drawer. The substitution panel is the same
    class and has one button, where a flex row of one changes nothing.

    `flex-wrap` rather than a media query: the drawer's width is set by the
    viewport and by how much the reader has open, so the point at which two
    Croatian labels stop fitting is not a number this file can know. Wrapped,
    they stack; unwrapped, they sit together. */
.coverpanel .form-actions
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

/*  The hint under the tick is an aside and not a caption: it says what the
    tick DOES - that one week means the lesson is split - and a reader deciding
    whether to press it needs it close to the tick rather than floating between
    two controls. */
.coverpanel .check + .coverhint
{
    margin-top: 6px;
    margin-bottom: 14px;
    line-height: 1.45;
}


/* ------------------------------------------------------------------ */
/* Move: swap or overlap, and the filter bars - 13 Sep 2026            */
/* ------------------------------------------------------------------ */

/*  THE CARD ABOUT TO BE COVERED, IN PALE RED - AND ONLY UNDER THE POINTER.

    Mišo asked for "blijedo crvenkasti shade na target kartici" and I painted
    EVERY card on the grid, permanently, the moment Overlap was chosen. His
    screenshot is a wall of red with a red border on almost every lesson, and
    his question is the only one that matters about it: "ne razumijem koja je
    razlika među crvenijim i onim koji to nisu?"

    There was none. A mark that is on everything marks nothing - it had become
    a background colour, and it hid the execution-type colours that are how a
    planner actually reads the week.

    He had said it plainly the first time and I read past it: "kad hover preko
    idem da one koje mogu overlap se malo zacrvene". So the red is the HOVER,
    which is exactly one card at a time - the one that is about to be covered -
    and the grid at rest looks like the grid.

    A WASH ON TOP, not a replacement: the subject, the type and the room stay
    legible through it, because the whole point of showing it is that the
    reader can see what they are covering. */
body.admin .card-over:hover,
body.admin .card-over:focus-visible
{
    box-shadow: inset 0 0 0 2px rgb(220 60 60 / .85);
}

body.admin .card-over:hover::after,
body.admin .card-over:focus-visible::after
{
    content: "";
    position: absolute;
    inset: 0;
    background: rgb(220 60 60 / .3);
    pointer-events: none;
    border-radius: inherit;
}

/*  AND SOMETHING QUIET AT REST, so the grid still says which cards are
    targets without shouting it: the pointer becomes a hand and the card lifts
    a little on hover. A person who has just pressed Overlap is looking for
    where to click, and a cursor is the cheapest possible answer. */
body.admin .card-over
{
    cursor: pointer;
}

/*  THE TWO RADIOS UNDER THE MOVE BUTTON.

    A fieldset, because that is what a group of radios with one question above
    them IS - the legend is read out with each option by a screen reader, so
    "Overlap" is heard as "When moved onto another lecture, Overlap" rather
    than as a loose word. Its default border and padding are cleared; the
    grouping is carried by the legend and the spacing. */
body.admin .movepick
{
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    margin: 0;
}

body.admin .dropwhat
{
    margin: 2px 0 0;
    padding: 0;
    border: 0;
}

body.admin .dropwhat > legend
{
    padding: 0;
    margin-bottom: 4px;
    font-size: 11px;
    font-weight: 650;
    letter-spacing: .03em;
    color: var(--ink-soft);
}

/*  ONE PER LINE. Side by side they fit in English and wrap in the middle of a
    word in Croatian, and a radio whose label has wrapped under the OTHER
    radio is a control people click wrong. */
body.admin .dropwhat > .check-inline
{
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    padding-block: 3px;
}

/*  THE FILTER BAR over a long picker - the substitute list and the room list
    share it. Wraps rather than squeezing: in a drawer this narrow the box, the
    tick and the button do not fit on one line in every language. */
body.admin .pickfilter
{
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 12px;
}

body.admin .pickfilter-find
{
    flex: 1 1 150px;
    min-width: 0;
    margin: 0;
}

body.admin .pickfilter-find > .input
{
    width: 100%;
}

body.admin .pickfilter > .check-inline
{
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    white-space: nowrap;
}

body.admin .pickfilter-count
{
    flex: 0 0 auto;
}

/*  A LABEL LEVEL WITH ITS BOX - Mišo, of the room panel: "Only this week
    labela nije poravnana sa checkboxom".

    The shared `.check` puts the box and the words in a row and leaves them on
    the baseline, which is right when the label is a paragraph and wrong when
    it is four words: the box sits a little high and the eye reads them as two
    things. `align-items: center` inside these panels, where every label is one
    short line. */
body.admin .coverpanel .check
{
    display: flex;
    align-items: center;
    gap: 8px;
}

/*  The box itself must not be squashed by the flex row when the label wraps -
    a checkbox that is 11px wide instead of 14 reads as a rendering fault. */
body.admin .coverpanel .check > input[type="checkbox"]
{
    flex: 0 0 auto;
    margin: 0;
}

/*  A screen-reader-only label. The find box has a visible placeholder, which
    is a hint and not a name: a placeholder disappears as soon as anybody types
    and is not reliably announced. */
.sr-only
{
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/*  THE ONE-VALUE FILTER IN A LIST BAR - the subjects list's aid code, 13 Sep
    2026. Narrow on purpose: every value in the estate is a single character,
    so a select sized for a room name would be a wide box holding one letter.
    It still grows for a longer code, because the column is VARCHAR(16) and a
    school may start using it properly tomorrow. */
body.admin .listtop-pick
{
    flex: 0 0 auto;
    margin: 0;
}

body.admin .listtop-pick > .input
{
    min-width: 8ch;
    width: auto;
}


/* ------------------------------------------------------------------ */
/* The substitute picker as a set of boxes, and room to breathe        */
/* ------------------------------------------------------------------ */

/*  THE ACTION ROW STOPS CROWDING WHAT IS ABOVE IT - Mišo, of a Save/Cancel
    pair sitting under a scrolling list of branches: "previše se drži donje
    linije - save i cancel".

    18px was enough under a plain field and is not under a box with its own
    border: the two edges land within a few pixels of each other and read as
    one crowded block rather than as a list and then a decision.

    ON THE SHARED CLASS, deliberately. Every editing screen in this application
    uses .form-actions and the note where it is declared says why they must
    look the same everywhere - a row that is roomier on one screen is how a set
    of screens stops feeling like one application. */
.form-actions
{
    margin-top: 26px;
    margin-bottom: 18px;
}

/*  THE CANDIDATE LIST, SCROLLED.

    A school with 263 lecturers cannot put them all in a drawer and still show
    the Save button, and paging a list somebody is picking two names out of
    would be worse than scrolling it. The filter bar above is the real answer;
    this is what makes the rest reachable.

    THE HEIGHT IS IN ROWS, NOT PIXELS - about eight of them - so it holds the
    same number of names whatever the reader's text size. */
body.admin .subpick
{
    max-height: 19rem;
    overflow-y: auto;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    padding: 4px 10px;
    background: color-mix(in srgb, var(--surface) 60%, transparent);
}

body.admin .subpick > .check
{
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0;
    padding-block: 6px;
}

body.admin .subpick > .check + .check
{
    border-top: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
}

body.admin .subpick > .check > input[type="checkbox"]
{
    flex: 0 0 auto;
    margin-top: 2px;
}

/*  The name on its own line and the load beneath it. Side by side they wrapped
    mid-sentence in the drawer's width, and a row whose second line is half a
    number is a row nobody reads. */
body.admin .subpick > .check > span
{
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

body.admin .subpick-none
{
    margin: 8px 2px;
}


/*  A TWO-CHARACTER BOX IS TWO CHARACTERS WIDE - the subject's filter aid,
    13 Sep 2026. Mišo: "najvise 2 slova su unutra". A field sized like a name
    and holding one letter tells the reader to write a sentence in it; the
    width IS the instruction, and the hint underneath only confirms it. */
body.admin .cnaid
{
    max-width: 7ch;
}


/* ------------------------------------------------------------------ */
/* The notice line, and the way back out of it - 19 Sep 2026           */
/* ------------------------------------------------------------------ */

/*
 * ONE LINE, TWO THINGS, AND THEY MUST NOT LOOK LIKE TWO LINES.
 *
 * The sentence at the top of an editing screen ("Room saved.") is where the
 * reader's eye already is after a write, so the control that takes the write
 * back belongs beside it rather than somewhere else on the page. The whole
 * design of this block is about making that read as ONE object: the action
 * sits inside the notice's own box, on its baseline, at its right edge.
 *
 * WHY THE BOX MOVED OUT TO THE WRAPPER. .notice used to draw the border, the
 * padding and the background itself. With a button beside it there were two
 * boxes on the line, of two different heights, with a gap between them that
 * no amount of margin made look deliberate. So the wrapper is the box now and
 * .notice inside it is bare text - which also means the colour rules
 * (.notice-ok, .notice-error) still do exactly what they did, because they
 * only ever set colour.
 */
body.admin .noticeline
{
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 0 0 14px;
    padding: 8px 10px 8px 14px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-m);
    background: var(--surface-2);
}

body.admin .noticeline > .notice
{
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
}

/*
 * THE LINE WITH NO NEWS ON IT IS QUIETER THAN THE ONE WITH NEWS.
 *
 * A reader who navigated away and came back can still take their last change
 * back, so the line is drawn then too - but it is not an announcement and
 * must not carry the weight of one. Same geometry, no border, so it reads as
 * a footnote to the screen rather than as something that just happened.
 */
body.admin .noticeline-bare
{
    border-color: transparent;
    background: none;
    padding-left: 0;
}

body.admin .noticeline-bare > .notice
{
    color: var(--ink-soft);
    opacity: 0.85;
}

/*
 * THE CONTROL ITSELF. Quiet, because it is not what the screen is for - and
 * present, because the one time somebody needs it they need it immediately.
 *
 * `white-space: nowrap` is load-bearing rather than tidy: the label is two or
 * three words in thirteen languages, and a control that wraps to two lines
 * makes the whole strip taller than the sentence beside it, which is how the
 * one-object illusion breaks.
 */
body.admin .undo-form
{
    flex: 0 0 auto;
    margin: 0;
}

body.admin .undo-btn
{
    display: inline-flex;
    align-items: center;
    gap: 7px;
    white-space: nowrap;
    font-size: 13px;
    padding-inline: 12px;
    border-color: color-mix(in srgb, var(--line-strong) 80%, transparent);
}

body.admin .undo-btn:hover
{
    color: var(--ink);
    border-color: var(--brand);
}

/*
 * THE ARROW, AT THE WEIGHT OF THE WORD NEXT TO IT.
 *
 * U+21B0 rather than an icon file: it is one character, it is in every font
 * this application already loads, and it needs no second HTTP request for a
 * control most readers will never press. Nudged up by a hair because the
 * glyph sits low in its box in most faces and otherwise reads as falling off
 * the baseline.
 */
body.admin .undo-mark
{
    font-size: 15px;
    line-height: 1;
    transform: translateY(-1px);
    opacity: 0.75;
}

body.admin .undo-btn:hover .undo-mark
{
    opacity: 1;
}

/*
 * WHY NOT, WHEN THE ANSWER IS NO.
 *
 * Where a greyed button would be, the reason stands instead. Mišo's standing
 * rule for these screens is that a refusal names what is in the way rather
 * than simply refusing - a disabled control says "no" and sends somebody
 * hunting for a fault that is not theirs.
 *
 * Right-aligned and capped, so a long sentence pushes the notice up into two
 * lines rather than stretching the strip across the whole screen.
 */
body.admin .undo-why
{
    flex: 0 1 auto;
    max-width: 38ch;
    margin: 0;
    text-align: right;
    font-size: 12.5px;
    line-height: 1.35;
    color: var(--ink-soft);
    opacity: 0.8;
}

/*
 * NARROW: THE ACTION GOES UNDER THE SENTENCE, NOT BESIDE IT.
 *
 * Side by side on a phone the button would squeeze the sentence to four
 * characters a line. Under it, left-aligned with the text above, and the
 * reason with it - the same order the reader reads in, which is the only
 * arrangement that does not need a second look.
 */
@media (max-width: 620px)
{
    body.admin .noticeline
    {
        flex-direction: column;
        align-items: stretch;
        gap: 9px;
    }

    body.admin .undo-why
    {
        max-width: none;
        text-align: left;
    }

    body.admin .undo-btn
    {
        width: 100%;
        justify-content: center;
    }
}
