/* Nixie shared component layer. Load after the surface's existing stylesheet.
   Keep tokens aligned with DESIGN.md; prefer intrinsic width for content. */
:root {
  --nx-surface: #ffffff;
  --nx-ground: #fdfaf7;
  --nx-text: #4a3f42;
  --nx-muted: #6f6368;
  --nx-border: #ebe0dd;
  --nx-accent: #94505d;
  --nx-sage-surface: #e7efe9;
  --nx-sage-text: #4a6553;
  --nx-radius: 8px;
  /* The Control Room launch ribbon. It is deliberately not a studio colour:
     the strip says "you arrived here from the owner portal", so it reads as
     the portal's own neutral rather than as part of the studio underneath it.
     The warn tint is used only to mark a read-only view, where what the owner
     may do is narrower than what the screen appears to offer. */
  --nx-ribbon-ground: #2b2b29;
  --nx-ribbon-ink: #fdfcf9;
  --nx-ribbon-quiet: #c9c6bd;
  --nx-ribbon-line: #4a4844;
  --nx-ribbon-warn: #f0c3a2;
  --nx-space-1: 4px;
  --nx-space-2: 8px;
  --nx-space-3: 12px;
  --nx-space-4: 16px;
  --nx-space-6: 24px;
  --nx-content-measure: 65ch;
}
/* Content hugs its contents, but can shrink and wrap in a narrow parent. */
.nx-fit, .nx-content-card, .nx-stat {
  box-sizing: border-box;
  inline-size: fit-content;
  max-inline-size: 100%;
  min-inline-size: 0;
  justify-self: start;
  align-self: start;
  overflow-wrap: anywhere;
}
.nx-fit-row { display: flex; flex-wrap: wrap; align-items: start; gap: var(--nx-space-4); }
.nx-fit-row > * { flex: 0 1 auto; min-inline-size: 0; max-inline-size: 100%; }
.nx-content-card { background: var(--nx-surface); color: var(--nx-text); border: 1px solid var(--nx-border); border-radius: var(--nx-radius); padding: var(--nx-space-4); }
.nx-content-card > :first-child { margin-block-start: 0; }
.nx-content-card > :last-child { margin-block-end: 0; }
.nx-content-card p { max-inline-size: var(--nx-content-measure); }
/* An inline statistic, never an empty full-width dashboard tile. */
.nx-stat { display: inline-flex; align-items: baseline; gap: var(--nx-space-2); color: var(--nx-text); font-variant-numeric: tabular-nums; white-space: normal; }
.nx-stat__value { font-size: 1.25em; font-weight: 750; line-height: 1.25; }
.nx-stat__label { color: var(--nx-muted); font-size: .875em; line-height: 1.4; }
/* The same sandbox notice is used in the site footer, account entry and studio. */
.nx-sandbox-banner { box-sizing: border-box; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; inline-size: fit-content; max-inline-size: 100%; padding: 18px 22px; background: var(--nx-sage-surface); color: var(--nx-sage-text); border-radius: var(--nx-radius); font-family: inherit; font-size: 14px; line-height: 1.5; }
.nx-sandbox-banner__copy { min-inline-size: 0; max-inline-size: var(--nx-content-measure); }
.nx-sandbox-banner__copy strong { display: block; font-weight: 750; }
.nx-sandbox-banner__copy > span { display: block; font-size: .9em; margin-block-start: 3px; }
.nx-sandbox-banner__action { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; max-inline-size: 100%; min-block-size: 42px; padding: 8px 14px; color: var(--nx-sage-text); border: 1px solid currentColor; border-radius: 5px; text-decoration: none; font-size: 13px; line-height: 1.3; font-weight: 700; background: transparent; }
.nx-sandbox-banner__action:hover { background: #dce8df; }
.nx-sandbox-banner__action:focus-visible { outline: 2px solid var(--nx-accent); outline-offset: 3px; }
.nx-sandbox-banner--compact { padding: 10px 26px; font-size: 12px; gap: 8px 18px; }
/* Explicit exception: a status ribbon spans the work area it describes. */
.nx-sandbox-banner--wide { inline-size: 100%; border-radius: 0; justify-content: space-between; }
@media (max-width:600px) { .nx-sandbox-banner { padding: 16px; gap: 12px; } .nx-sandbox-banner--compact { padding: 10px 16px; font-size: 11px; gap: 4px 12px; } }

/* ==========================================================================
   Loading. Nixie never uses a spinner; see DESIGN.md. There are exactly two
   answers here: the squiggle, for a whole page or view that is on its way, and
   a content placeholder, for a region whose shape is already known.
   ========================================================================== */

/* The squiggle. A hand-drawn water line across the top of the viewport, 3px
   down from the edge and 10px tall so it clears a top bar's wordmark, spanning its full width. It is drawn, not filled: the
   dash travels along the path, so the wave writes itself across the screen and
   recedes, and at no point does it claim to know how far along a load is. */
.nx-loading-line {
  position: fixed;
  inset-block-start: 3px;
  inset-inline: 0;
  z-index: 2147483000; /* above studio sheets, dialogs and toasts */
  block-size: 10px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .28s ease;
}
.nx-loading-line.is-loading { opacity: 1; }
/* assets/loading.js draws the wave to the viewport's own width in whole
   periods, so this fills its container at very close to 1:1 and a period keeps
   its drawn size on a phone as on a desktop. */
.nx-loading-line__wave { display: block; inline-size: 100%; block-size: 100%; }
.nx-loading-line__wave path {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  /* The hero threads' own weights (hero-flow.css .thread-bed / .thread-run):
     a 1.2 bed and a 1.6 run. Path units are CSS pixels. */
  stroke-width: 1.6;
  stroke-linecap: round;
  /* pathLength is normalised to 1000 in the markup, so these are portions of
     the whole line regardless of how many periods the viewport holds.
     The drawn segment is most of the line, not a short dart: the wave should
     read as crossing the content block it sits above, which is what the
     reader is actually waiting for. */
  stroke-dasharray: 880 1000;
  stroke-dashoffset: 1880;
}
/* Juice. The stroke is the brand mark, so it is allowed a little soul: it
   enters briskly, eases through the middle where the reader can read it, and
   releases on the way out, rather than crossing at one constant rate. The
   trailing width taper and the settle below give the line follow-through —
   the non-essential detail that makes a mark feel drawn by a hand rather
   than stepped by a timer. Slower than the old 1.9s, because the line now
   travels the whole width and a fast crossing reads as a nervous tic. */
.nx-loading-line.is-loading .nx-loading-line__wave path {
  animation: nx-loading-draw 2.9s cubic-bezier(.62,.03,.31,1) infinite,
             nx-loading-breathe 2.9s ease-in-out infinite;
}
@keyframes nx-loading-draw {
  from { stroke-dashoffset: 1880; }
  to   { stroke-dashoffset: -880; }
}
/* Secondary motion: the stroke thickens as the wave arrives and thins as it
   leaves, the way pressure changes across a drawn line. */
@keyframes nx-loading-breathe {
  0%   { stroke-width: 1.2; opacity: .55; }
  22%  { stroke-width: 1.9; opacity: 1; }
  70%  { stroke-width: 1.7; opacity: 1; }
  100% { stroke-width: 1.2; opacity: .5; }
}
/* The bed: the whole line, pale and still, under the run. It takes part in
   the snap (it has to pull straight with the run) and in nothing else. */
.nx-loading-line__wave path.nx-loading-line__bed {
  stroke: var(--nx-loading-bed, #ebe0dd);
  stroke-width: 1.2;
  stroke-dasharray: none;
  stroke-dashoffset: 0;
}
.nx-loading-line.is-loading .nx-loading-line__wave path.nx-loading-line__bed { animation: none; }
/* ------------------------------------------------------------- the snap
   The wave is work that is still loose. The moment it has landed, the line
   pulls straight: the whole product is loose work coming into order, so its
   loading state performs that rather than fading out on the same wave it
   started on.

   It does not arrive politely. The line overshoots the midline and settles
   back — the elastic beat that makes the arrival feel like a thing landing
   instead of an animation ending. assets/loading.js writes both shapes onto
   the element as --nx-wave and --nx-flat; they are the same cubics in the
   same order, differing only in where the control points sit, which is what
   allows one to be interpolated into the other.

   The dash is released at the same moment, so the settled line is whole
   rather than a fragment of one frozen mid-travel. */
.nx-loading-line.is-settled .nx-loading-line__wave path {
  stroke-dasharray: none;
  stroke-dashoffset: 0;
  /* linear, because the curve is already in the keyframes: they are samples
     of a solved spring, so easing them again would bend a shape that is
     already the right shape. */
  animation: nx-loading-snap .52s linear forwards;
}
/* Sampled from the spring integrator in assets/glance-player.js at damping
   11, stiffness 210, mass 1 — the same solver the Powermove scenes use. The
   percentages are where that curve actually is at each point, not values
   chosen to look springy: it crosses flat at about 28%, reaches the far side
   of its overshoot near 48%, and comes to rest. */
@keyframes nx-loading-snap {
  0%   { d: var(--nx-wave); stroke-width: 1.7; opacity: 1; }
  16%  { stroke-width: 2.15; }
  28%  { d: var(--nx-flat); stroke-width: 2.48; }
  48%  { d: var(--nx-over); stroke-width: 2.36; }
  72%  { d: var(--nx-flat); stroke-width: 2.6; }
  100% { d: var(--nx-flat); stroke-width: 1.6; opacity: .85; }
}
/* Where `d` cannot be interpolated the line still straightens, it simply
   does it by scaling the wave flat instead of tweening the path. */
@supports not (d: path("M0 0")) {
  .nx-loading-line.is-settled .nx-loading-line__wave {
    transform: scaleY(.04);
    transform-origin: 50% 50%;
    transition: transform .52s cubic-bezier(.22,1.4,.36,1);
  }
}

/* Reduced motion: the same squiggle, drawn once and left there. The line still
   says "this page is coming"; it simply does not travel to say it. The snap
   is motion for its own sake, so it is dropped entirely and the settled line
   is simply shown straight. */
@media (prefers-reduced-motion: reduce) {
  .nx-loading-line__wave path { stroke-dasharray: none; stroke-dashoffset: 0; opacity: .55; stroke-width: 2; }
  .nx-loading-line.is-loading .nx-loading-line__wave path { animation: none; }
  .nx-loading-line.is-settled .nx-loading-line__wave path { animation: none; d: var(--nx-flat); }
  .nx-loading-line.is-settled .nx-loading-line__wave { transform: none; transition: none; }
  .nx-loading-line { transition: none; }
}

/* ==========================================================================
   The drawn frame. The same hand as the squiggle, but shaped like the thing
   that is coming rather than laid across the top of it. A view that knows it
   is about to be a card, a table or a panel draws that outline in one
   continuous rose stroke, then lets a few short interior lines arrive after
   it — the rhythm of the rows the region is about to hold.

   This is the squiggle's second form, for loading an INTERFACE. The
   horizontal line says "this page is on its way". The drawn frame says "this
   region is about to be *this shape*", which is the more useful statement
   once the reader is already inside a surface. It is still drawn, never
   filled, and it still reports no fraction: the outline completes and the
   interior lines settle, and that is the whole vocabulary.
   ========================================================================== */
.nx-draw {
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  block-size: auto;
  overflow: visible;
}
.nx-draw__frame,
.nx-draw__line {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  stroke-linecap: round;
  stroke-linejoin: round;
  /* The drawing stretches to the region it describes, but the stroke must not
     stretch with it, or the outline thickens on one axis and the hand is lost. */
  vector-effect: non-scaling-stroke;
}
/* The outline. One stroke, drawn from the top-left and closing on itself. */
.nx-draw__frame {
  stroke-width: 2;
  /* pathLength="1000" is set on the element in markup, so the dash below is a
     portion of the whole outline whatever the region's real dimensions are. */
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: nx-draw-frame 1.5s cubic-bezier(.65,0,.35,1) forwards;
}
@keyframes nx-draw-frame {
  from { stroke-dashoffset: 1000; }
  to   { stroke-dashoffset: 0; }
}
/* The interior lines. Impact, not content: they arrive after the outline has
   closed, each a beat behind the last, and they are shorter and lighter than
   the frame so they read as the suggestion of rows rather than fake records.
   The stagger is the juice — the region assembles instead of appearing. */
.nx-draw__line {
  stroke-width: 1.6;
  opacity: 0;
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: nx-draw-line .85s cubic-bezier(.4,0,.2,1) forwards;
  animation-delay: calc(1.15s + var(--nx-draw-index, 0) * .16s);
}
@keyframes nx-draw-line {
  from { stroke-dashoffset: 1000; opacity: 0; }
  60%  { opacity: .75; }
  to   { stroke-dashoffset: 0; opacity: .62; }
}
/* While the region is still waiting, the whole drawing breathes once it has
   settled, so a long wait does not end in a static picture that looks broken. */
.nx-draw.is-waiting { animation: nx-draw-settle 2.6s ease-in-out 2.4s infinite; }
@keyframes nx-draw-settle {
  0%, 100% { opacity: 1; }
  50%      { opacity: .58; }
}
/* Reduced motion: the same drawing, complete and still. The shape is the
   message, and the shape survives without any of the drawing. */
@media (prefers-reduced-motion: reduce) {
  .nx-draw__frame,
  .nx-draw__line { animation: none; stroke-dashoffset: 0; opacity: 1; }
  .nx-draw__line { opacity: .6; }
  .nx-draw.is-waiting { animation: none; }
}

/* ==========================================================================
   The traced mark. The squiggle's third form: the same wave, closed around
   the Nixie mark instead of laid across the top of a page.

   It exists for the one moment where there is nothing else to describe. On
   the sandbox entry the visitor has asked for a studio and the product is
   fetching it; there is no page on its way that the reader is already
   looking at, and no region whose shape is known, so the horizontal line has
   nothing to sit above and the drawn frame has nothing to outline. What is on
   screen is the mark, and the line runs around it.

   It obeys the same rule as the other two: drawn, never filled, and reporting
   no fraction. The dash travels around the closed path, so a stroke is always
   arriving somewhere on the ring and leaving somewhere else, and the ring is
   never completed into a static border.
   ========================================================================== */
.nx-trace {
  position: relative;
  display: block;
  inline-size: 150px;
  block-size: 150px;
  max-inline-size: 100%;
}
/* The mark is centred and sized as its own share of the viewBox, set inline
   by assets/loading.js, so the gap between the mark and the wave holds
   whatever size the container is given. */
.nx-trace__mark {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
}
/* The wave overflows its box by design: the crests sit outside the padded
   viewBox at the extremes of the breathe animation. */
.nx-trace__wave {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}
.nx-trace__line {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  stroke-linecap: round;
  stroke-width: 1.5;
  /* pathLength is normalised to 1000 in the markup. The drawn segment is
     nearly the whole ring, with a short travelling break in it: a line that
     is mostly present reads as one continuous stroke circling the mark, where
     a 560/440 split read as a stroke that kept running out and starting
     again. The gap is what moves, rather than the stroke. */
  stroke-dasharray: 880 120;
  stroke-dashoffset: 1000;
  /* Linear, because a circle has no beginning: an eased curve gives the loop
     a fast part and a slow part, and the eye finds the seam. Slower than the
     horizontal line at 2.9s, since this one is followed round rather than
     watched past. */
  animation: nx-trace-draw 4.2s linear infinite,
             nx-trace-breathe 4.2s ease-in-out infinite;
}
@keyframes nx-trace-draw {
  /* One full period of the dash pattern, so the loop closes on itself with
     no jump at the repeat. */
  from { stroke-dashoffset: 1000; }
  to   { stroke-dashoffset: 0; }
}
/* The same pressure change as the horizontal line, at the lighter weight this
   form is drawn in. */
@keyframes nx-trace-breathe {
  0%   { stroke-width: 1.1; opacity: .5; }
  22%  { stroke-width: 1.9; opacity: 1; }
  70%  { stroke-width: 1.8; opacity: 1; }
  100% { stroke-width: 1.1; opacity: .45; }
}
/* Reduced motion: the whole ring, drawn once and left around the mark. The
   line still says the studio is coming; it simply does not travel to say it. */
@media (prefers-reduced-motion: reduce) {
  .nx-trace__line { animation: none; stroke-dasharray: none; stroke-dashoffset: 0; stroke-width: 1.5; opacity: .6; }
}

/* The verification widget while it is verifying on its own. Turnstile draws
   its own spinner beside "Verifying…", inside a cross-origin frame where it
   cannot be restyled, and a rotating shape is banned on every Nixie surface.
   assets/verification.js therefore keeps the widget unpainted until it is
   clear that it actually wants the visitor, and reveals it then. The box keeps
   its geometry throughout, because Turnstile needs somewhere to lay out and
   because revealing it must not shift what is around it. */
.verification.is-quiet { visibility: hidden; }

/* Content placeholders. A region that knows its own shape says so while it
   waits: table rows keep their rhythm, a card keeps its measure. The shimmer
   is a slow wash across the placeholder, not a rotating object. */
.nx-placeholder {
  display: block;
  block-size: 1em;
  border-radius: var(--nx-radius-sm, 4px);
  background: var(--nx-placeholder, #ece2de);
  background-image: linear-gradient(90deg, transparent 0%, var(--nx-placeholder-sheen, rgb(255 255 255 / 60%)) 50%, transparent 100%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: nx-placeholder-wash 1.6s ease-in-out infinite;
}
@keyframes nx-placeholder-wash {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-placeholder { animation: none; background-image: none; }
}
/* A stack of lines standing in for a paragraph or a record. The last line is
   short, the way a real last line is. */
.nx-placeholder-lines { display: flex; flex-direction: column; gap: var(--nx-space-3); margin: 0; padding: 0; list-style: none; }
.nx-placeholder-lines > * { inline-size: min(100%, 600px); }
.nx-placeholder-lines > :nth-child(2) { inline-size: min(80%, 470px); }
.nx-placeholder-lines > :nth-child(3) { inline-size: min(65%, 360px); }
.nx-placeholder-lines > :nth-child(4) { inline-size: min(72%, 410px); }
/* A table waiting for rows keeps the row rhythm it is about to have, so the
   records do not jump into place on arrival. */
.nx-placeholder-rows { display: flex; flex-direction: column; gap: var(--nx-space-4); margin: 0; padding: var(--nx-space-2) 0; list-style: none; }
.nx-placeholder-rows > * { display: flex; gap: var(--nx-space-4); align-items: center; }
.nx-placeholder-rows .nx-placeholder { flex: 1 1 0; min-inline-size: 0; }
.nx-placeholder-rows .nx-placeholder--narrow { flex: 0 1 90px; }
/* The caption under a placeholder names what is coming, in the surface's own
   words. It is a statement, never the word "Loading" beside a moving shape. */
.nx-placeholder-note { color: var(--nx-muted); font-size: .8125em; margin: var(--nx-space-3) 0 0; }


/* ==========================================================================
   The rail's pin. Every surface with a navigation rail shares this: the
   studio sheet, the desk and backstage each had their own copy, and the
   three had drifted into three behaviours. assets/rail-pin.js owns the
   state; this owns how it looks.

   Every rail carries .nx-rail so one selector reaches all of them. The
   surface-specific class stays for the layout each one already has.
   ========================================================================== */
/* The top row is a heading row, not a control. It used to be one button that
   was the word "Operations" and the pin at once, which read as the odd one
   out: every row below it is a destination, and this one did something else
   entirely when pressed. The owner asked for it to look like the rows it sits
   above — an icon in the icon column, the label on the same left edge as the
   labels below — with the pin as its own thing at the right-hand end.

   There is no Operations landing view in management.js (the views are day,
   people, outlook, payments, direct-debit, year, welcome and alumni), so the
   row is a plain heading and not focusable. The pin is the only button in it. */
.nx-rail .rail-toggle { position: relative; justify-content: flex-start; cursor: default; }
/* The bars stay in the icon column in every state. Collapsed they are the
   only thing there is; open they are the section's mark, the way each row
   below has one. They used to disappear the moment the rail opened, which
   left the heading with no icon and its label out of line with the rest. */
.nx-rail .rail-bars { flex: none; }

/* The pin sits at the right-hand end of the row and appears with the label,
   because a pin on a 56px rail has nothing to be at the end of. */
.nx-rail .rail-toggle .rail-pin {
  display: none;
  flex: none;
  margin-inline-start: auto;
  padding: 5px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  opacity: .6;
  cursor: pointer;
  transition: opacity .12s ease;
}
.nx-rail.is-expanded .rail-toggle .rail-pin,
.nx-rail.is-pinned .rail-toggle .rail-pin,
.backstage-rail:hover .rail-toggle .rail-pin,
.backstage-rail:focus-within .rail-toggle .rail-pin { display: inline-flex; }
.nx-rail .rail-toggle .rail-pin:hover { opacity: 1; background: var(--nx-linen, #f0eee8); }
.nx-rail .rail-toggle .rail-pin:focus-visible { display: inline-flex; opacity: 1; outline: 2px solid currentColor; outline-offset: -2px; }

/* The rail names itself once it is open. Collapsed there are 56 pixels and an
   icon already saying navigation lives here, so the word would only be
   clipped; open, the top row is the heading the sections below it sit under.
   It is the owner's word — Operations — and never "Studio navigation".

   The bars icon stays in the icon column in every state, so the heading sits
   on exactly the same left edge as the labels below it with no nudge: the row
   gets its alignment from the same icon-then-label shape as its neighbours,
   rather than from a correction that has to be re-tuned when a glyph changes.

   The surface stylesheets fade every span in a rail row from opacity 0, which
   is how the section labels arrive. The heading is shown by display instead —
   it does not exist while the rail is shut — so it sets its own opacity back
   rather than inheriting a fade it is not part of. */
.nx-rail .rail-toggle .rail-heading { display: none; font-weight: 600; color: var(--nx-text, #30302e); white-space: nowrap; }
.nx-rail.is-expanded .rail-toggle .rail-heading,
.nx-rail.is-pinned .rail-toggle .rail-heading,
/* Backstage widens each row rather than the rail, so it opens on hover and
   focus in CSS as well as on the shared .is-expanded class. */
.backstage-rail:hover .rail-toggle .rail-heading,
.backstage-rail:focus-within .rail-toggle .rail-heading { display: block; opacity: 1; transform: none; }
/* Unpinned the pin sits at an angle, the way one waiting to be pushed in
   does; pinned it stands upright in the studio's rose. The glyph turns, not
   the button: rotating the button would turn its hit area with it. */
.nx-rail:not(.is-pinned) .rail-toggle .rail-pin svg { rotate: -32deg; }
.nx-rail.is-pinned .rail-toggle .rail-pin { color: var(--rose-deep); }

/* Pinned, the rail keeps its full width and the workspace starts after it
   rather than underneath it: the point of pinning is that nothing is covered
   while the reader works. */
/* The shell below owns the pinned column, alongside its content region. */

/* A section with views of its own — Payments has fee records and direct debit
   — lists them under it. Collapsed there is no room for them and nothing to
   read them against, so they exist only once the rail is open: the flyout is
   where a section's inner views become reachable without going to the page
   first and finding the tab strip there. They are indented rather than
   iconed, because the icon belongs to the section above them. */
.nx-rail .rail-sub { display: none; }
.nx-rail.is-expanded .rail-sub,
.nx-rail.is-pinned .rail-sub { display: block; }
/* The surface stylesheets reach their own rows as `.manager .rail-link` and
   the like, so the indent has to out-specify that rather than merely follow
   it in the cascade. */
.nx-rail .rail-sub .rail-link--sub { min-height: 38px; padding-block: 8px; padding-inline-start: 53px; font-size: 11px; color: var(--nx-muted, #68665f); }
.nx-rail .rail-sub .rail-link--sub[aria-current="page"] { color: inherit; font-weight: 700; }
/* The sub-view labels are shown with the rail, not faded in with the section
   labels above them: the row they sit in does not exist until the rail opens. */
.nx-rail .rail-link--sub span { opacity: 1; transform: none; }

/* --------------------------------------------- a parent is not the page
   Fee records is a view inside Payments, so on the fee records page two rows
   were drawn as selected: the sub-item, correctly, and Payments above it,
   which is not the page the reader is on. Two selected rows in one menu say
   the reader is in two places.

   Only the leaf carries aria-current="page" and the selected fill.

   And with the rail OPEN, the parent gets nothing at all: no fill, no bar,
   and no bold either. It used to keep the weight of a heading, which the
   owner read as still being selected — "If I'm in Payments / Direct debit, I
   do not expect to see a bold Payments parent." The sub-item directly beneath
   it is already marked, and it is the only thing that should be: the parent
   is a row you are not on, so it looks like every other row you are not on.

   Collapsed is the exception, and only because the sub-items are not drawn
   there. With the children out of sight the parent IS the section the reader
   is in, and something has to say so, so its icon keeps a quiet tint of the
   same rose. That is data-rail-within, set by the surface that knows which
   view is open. */
.nx-rail .rail-link[data-rail-within] {
  background: none;
  box-shadow: none;
  color: inherit;
  font-weight: inherit;
}
.nx-rail:not(.is-expanded):not(.is-pinned) .rail-link[data-rail-within] { background: var(--rose-wash, #f2e4e6); color: var(--rose-deep, #94505d); }
/* Backstage opens each row rather than the rail, so its collapsed state is
   "not hovered and not focused within" rather than a class on the aside. */
.backstage-rail:hover .rail-link[data-rail-within],
.backstage-rail:focus-within .rail-link[data-rail-within] { background: none; color: var(--nx-text, #30302e); }

/* ============================================ the selected row is mauve
   The row the reader is on was a grey-brown fill — #e6e3da, written out in
   studio.css, management.css and notebook.css alike — which is the same
   stone the rail's own hover is, only a shade darker. It said "this row is
   slightly different" rather than "you are here", and it said it in a colour
   Nixie does not otherwise use for meaning.

   Selection is brand now: a pale rose fill with the label and the icon in the
   text-safe rose above it. The tokens are the ones the rest of the app already
   selects with — .nx-nav a[aria-current], the fee-records tab — so a selected
   thing looks the same wherever it is:

     fill   --rose-wash  #f2e4e6
     ink    --rose-deep  #94505d      4.77:1 on the fill, above the 4.5:1 floor
     hover  --rose-faint #faf1f2      5.31:1, a lighter tint of the same

   This file is loaded by every surface with a rail, and these selectors carry
   .nx-rail, so they out-specify the surface stylesheets' own
   `.manager .rail-link[aria-current=page]` and the like without those having
   to be edited in four places. The fallbacks are for the notebook, which does
   not load app/nixie.css and so has no token block of its own. */
.nx-rail .rail-link[aria-current="page"] {
  background: var(--rose-wash, #f2e4e6);
  color: var(--rose-deep, #94505d);
  font-weight: 700;
}
.nx-rail .rail-link[aria-current="page"] svg { color: var(--rose-deep, #94505d); }
/* Hover is a lighter tint of the same rose, so moving down the rail reads as
   one family rather than as stone that turns rose when it lands. The selected
   row keeps its own fill when hovered: it is already where the reader is. */
.nx-rail .rail-link:hover { background: var(--rose-faint, #faf1f2); }
.nx-rail .rail-link[aria-current="page"]:hover { background: var(--rose-wash, #f2e4e6); }
/* Backstage draws the selected row with a bar rather than a fill. It gets the
   fill too, so all four surfaces say "you are here" the same way. */
.nx-rail .rail-link[aria-current="page"] { box-shadow: inset 3px 0 0 var(--rose-deep, #94505d); }
.nx-rail .rail-link--sub[aria-current="page"] { box-shadow: none; }
/* The sub-item is a leaf inside an open section, and it carries the selection
   for the section: the same rose, without the bar the top-level rows take. */
.nx-rail .rail-sub .rail-link--sub[aria-current="page"] { background: var(--rose-wash, #f2e4e6); color: var(--rose-deep, #94505d); font-weight: 700; }

/* Operations is a destination: inactive ink matches the adjacent icons. */
.nx-rail .rail-bars { color: inherit; }

/* ------------------------------------------ a rail that cannot be pinned
   The notebook writes across the full width of its page: there is no column
   to give away, so its rail says data-rail-pin="never". Then there is no pin
   to show, the rail never takes a layout column, and it lies over the content
   on hover instead. assets/rail-pin.js honours the same attribute and leaves
   the stored preference alone, so pinning on the desk survives a visit here. */
.nx-rail[data-rail-pin="never"] .rail-pin { display: none !important; }
.nx-rail[data-rail-pin="never"] { position: absolute; }
.nx-rail[data-rail-pin="never"].is-pinned { position: absolute; width: 56px; flex: none; }
.nx-rail[data-rail-pin="never"].is-expanded { width: 212px; }

/* ==========================================================================
   The select — one rule, every surface

   This file is the only stylesheet every Nixie surface loads (the notebook
   does not load app/nixie.css), so the select is drawn here and nowhere
   else. Four surfaces had their own copy — app/nixie.css `.nx-select`, the
   sandbox footer's "View as", the studio's request sort, and the desk's bare
   `select` with the operating system's triangle — and the four had drifted.

   What it is: a thin-line chevron in place of the browser's solid triangle,
   inset 11px from the trailing edge with 34px of padding behind it, on a
   uniform 1px border. The chevron is an inline SVG in the background, so it
   takes no image request.

   What it is not: a rule down the leading edge. An earlier pass gave the
   select a 2px rose border-inline-start as a "branded" mark. It made every
   select look like a quoted block or a validation error, and no other Nixie
   control carries one. All four borders are the same 1px line.
   ========================================================================== */
/* One list, written once. `.nx-select` is the class a surface can ask for by
   name; the rest are the studio screens' bare selects, which reach the same
   drawing without every call site having to remember the class. */
:is(
  .nx-select,
  .nx-sandbox-footer .sandbox-perspective select,
  .studio-sheet select,
  .manager-sheet select,
  .backstage-sheet select,
  .notebook-shell select,
  .class-drawer select,
  .priority-dialog select,
  .detail-dialog select,
  .backstage-dialog select,
  .inquiry-fields select
) {
  appearance: none;
  -webkit-appearance: none;
  box-sizing: border-box;
  max-inline-size: 100%;
  /* Room for the chevron at the trailing edge. All four borders match. */
  padding-inline: 15px 34px;
  border: 1px solid var(--nx-select-border, var(--line-strong, #b9b6ad));
  border-radius: var(--nx-select-radius, 4px);
  background-color: var(--nx-select-ground, var(--paper, #fdfcf9));
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b5d61' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5 12 15.5 18 9.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px 15px;
  color: inherit;
  font: inherit;
  font-size: var(--nx-select-font-size, inherit);
}
[dir="rtl"] :is(
  .nx-select,
  .nx-sandbox-footer .sandbox-perspective select,
  .studio-sheet select,
  .manager-sheet select,
  .backstage-sheet select,
  .notebook-shell select,
  .class-drawer select,
  .priority-dialog select,
  .detail-dialog select,
  .backstage-dialog select,
  .inquiry-fields select
) { background-position: left 11px center; }
:is(
  .nx-select,
  .nx-sandbox-footer .sandbox-perspective select,
  .studio-sheet select,
  .manager-sheet select,
  .backstage-sheet select,
  .notebook-shell select,
  .class-drawer select,
  .priority-dialog select,
  .detail-dialog select,
  .backstage-dialog select,
  .inquiry-fields select
)::-ms-expand { display: none; }
/* Focus darkens and thickens the chevron; the ring itself is the surface's
   own :focus-visible outline, which is already the studio's rose. */
:is(
  .nx-select,
  .nx-sandbox-footer .sandbox-perspective select,
  .studio-sheet select,
  .manager-sheet select,
  .backstage-sheet select,
  .notebook-shell select,
  .class-drawer select,
  .priority-dialog select,
  .detail-dialog select,
  .backstage-dialog select,
  .inquiry-fields select
):focus { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394505d' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5 12 15.5 18 9.5'/%3E%3C/svg%3E"); }
/* A select in a toolbar or a compact filter row is the same control at a
   smaller size, so the chevron and its gutter scale together rather than the
   chevron crowding the edge. A surface asks for it by name; it never redraws
   the control to get a smaller one. */
.nx-select--compact { padding-block: 5px; padding-inline: 9px 28px; background-position: right 8px center; background-size: 13px 13px; }
[dir="rtl"] .nx-select--compact { background-position: left 8px center; }

/* ==========================================================================
   The branded scrollbar — .nx-scrollbar

   A panel that scrolls inside the studio sheet (incoming requests, a rail, a
   drawer) is a region of Nixie, not a region of the operating system, and the
   grey slab the browser draws there is the loudest thing in the panel. This
   draws a thin mauve thumb on a track that is not drawn at all.

   Standards first: scrollbar-width and scrollbar-color are what Firefox and
   current Chromium honour, and they are the whole of the rule for them.
   Safari has neither, so the ::-webkit-scrollbar fallback repeats the same
   two colours behind @supports not (scrollbar-color: auto) — the guard keeps
   the legacy pseudo-elements away from browsers that already did it properly
   and would otherwise take the fallback's fixed metrics instead.

   A browser with neither keeps its own bar. Nothing here is JavaScript: a
   scrollbar drawn in script stops being a scrollbar the moment the page is
   busy, and it takes the keyboard and the wheel with it.

   The page's own scrollbar is untouched — the home screen keeps the bar the
   reader's browser gave it. This is for panels inside a sheet, named here
   once so the four work surfaces cannot drift into four scrollbars.
   ========================================================================== */
:root {
  --nx-scroll-thumb: #c6a7ae;        /* rose, lightened to sit under content */
  --nx-scroll-thumb-hover: #94505d;  /* deep rose, the text-safe accent */
  --nx-scroll-track: transparent;    /* the panel's own ground shows through */
}
/* `.nx-scrollbar` is the class a new scroll region asks for by name. The rest
   are the inner scroll regions that already exist: the studio's incoming
   requests and its schedule, backstage's workspace and record editor, the
   desk's work area, and the notebook's class drawer. */
:is(
  .nx-scrollbar,
  .request-pane,
  .schedule-pane,
  .backstage-workspace,
  #editor-body,
  .manager-sheet main,
  .class-drawer
) {
  scrollbar-width: thin;
  scrollbar-color: var(--nx-scroll-thumb) var(--nx-scroll-track);
}
@supports not (scrollbar-color: auto) {
  :is(.nx-scrollbar, .request-pane, .schedule-pane, .backstage-workspace, #editor-body, .manager-sheet main, .class-drawer)::-webkit-scrollbar { inline-size: 8px; block-size: 8px; }
  :is(.nx-scrollbar, .request-pane, .schedule-pane, .backstage-workspace, #editor-body, .manager-sheet main, .class-drawer)::-webkit-scrollbar-track { background: var(--nx-scroll-track); }
  :is(.nx-scrollbar, .request-pane, .schedule-pane, .backstage-workspace, #editor-body, .manager-sheet main, .class-drawer)::-webkit-scrollbar-thumb { background: var(--nx-scroll-thumb); border-radius: 4px; }
  :is(.nx-scrollbar, .request-pane, .schedule-pane, .backstage-workspace, #editor-body, .manager-sheet main, .class-drawer)::-webkit-scrollbar-thumb:hover { background: var(--nx-scroll-thumb-hover); }
  :is(.nx-scrollbar, .request-pane, .schedule-pane, .backstage-workspace, #editor-body, .manager-sheet main, .class-drawer)::-webkit-scrollbar-corner { background: transparent; }
}
/* Firefox and Chromium have no hover selector for the thumb, so the whole
   region darkens its thumb token while the pointer is in it. */
:is(
  .nx-scrollbar,
  .request-pane,
  .schedule-pane,
  .backstage-workspace,
  #editor-body,
  .manager-sheet main,
  .class-drawer
):hover { scrollbar-color: var(--nx-scroll-thumb-hover) var(--nx-scroll-track); }

/* ==========================================================================
   The emulated browser window — its parts

   The timetable and the desk both show the studio inside a drawing of a
   browser window. The strip that holds the parts is each surface's own, but
   the parts are one set, here, because two copies of a window become two
   windows the first time one of them is touched.

   Everything except the two close controls is aria-hidden and inert: the
   padlock, the back / forward / reload glyphs and the tab's own close cross
   are 1px hairlines that state nothing about the connection and offer no
   control the product does not have. The address reads nixiedance.com
   rather than https://, for the same reason.
   ========================================================================== */
/* The parts of the OLD two-row frame — the red traffic light that doubled as
   a close, the tab and its close cross, the separate top-right close, and the
   toolbar glyphs keyed on their own names — are gone. The window is one row
   now, drawn to the home page's design, and its parts are .nx-window-* at the
   foot of this file. */

/* ==========================================================================
   A REAL STUDIO IS NOT SHOWN INSIDE A DRAWN BROWSER

   The emulated browser window — traffic lights, the tab, the back and forward
   glyphs, the address pill, the close controls — together with the grey
   surround it floats on and the sandbox footer beneath it, exist to say ONE
   thing: what you are looking at is a demonstration. They frame harlow, and
   the sandbox a visitor gets from "Try first" on the marketing site.

   A real studio is not a demonstration of Nixie, it IS Nixie: the owner's own
   studio, a Control Room launch, a read-only view, a guest pass. Drawing a
   picture of a browser around it says the opposite, and wastes a band of the
   viewport at the top and the bottom of a page the studio works in all day.
   So a real studio takes the whole screen, edge to edge, with the header and
   the rail flush to the viewport and the content region using the full height.

   web/src/hooks.server.js decides which it is from the session THAT REQUEST
   ALREADY FETCHES, and writes is-demo-studio or is-real-studio onto the body
   before the first byte of HTML is sent. It has to be that early: deciding it
   in the browser means a real studio wears the frame until a round trip comes
   back, and the owner sees it flash.

   One block, because all four surfaces share the pieces even though each
   draws its sheet a little differently.
   ========================================================================== */
body.is-real-studio .nx-window-bar,
body.is-real-studio .nx-sandbox-footer { display: none !important; }

/* No surround, no padding, no scroll trap: the sheet IS the page.

   The one thing that may still push the page down is the launch ribbon, which
   a Control Room launch, a read-only view and a guest pass all carry. It is
   fixed to the top of the viewport, so the body owes it that much padding or
   the studio header sits underneath it. LaunchRibbon.svelte measures itself
   into --nx-ribbon-height and adds has-launch-ribbon; without a ribbon the
   property is unset and this is a plain zero. */
body.is-real-studio { padding: 0 0 0 0 !important; background: var(--paper, #fdfcf9) !important; }
body.is-real-studio.has-launch-ribbon { padding-block-start: var(--nx-ribbon-height, 0px) !important; }
body.is-real-studio .studio-sheet,
body.is-real-studio .manager-sheet,
body.is-real-studio .backstage-sheet,
body.is-real-studio .notebook-shell {
  inline-size: 100vw;
  max-inline-size: none;
  block-size: 100dvh;
  min-block-size: 100dvh;
  margin: 0;
  border: 0;
  border-radius: 0;
}
/* With a ribbon, the sheet gets what is left of the viewport rather than all
   of it, or its last rows would be pushed off the bottom by the padding. */
body.is-real-studio.has-launch-ribbon .studio-sheet,
body.is-real-studio.has-launch-ribbon .manager-sheet,
body.is-real-studio.has-launch-ribbon .backstage-sheet,
body.is-real-studio.has-launch-ribbon .notebook-shell {
  block-size: calc(100dvh - var(--nx-ribbon-height, 0px));
  min-block-size: calc(100dvh - var(--nx-ribbon-height, 0px));
}
/* The footer reserved space through a custom property; with no footer there
   is nothing to reserve, and the last rows of a table were sitting above a
   band of nothing. */
body.is-real-studio { --nx-sandbox-height: 0px; }
body.is-real-studio.has-sandbox-frame { padding-block-end: 0 !important; }

/* ==========================================================================
   The guided tour — assets/tour.js

   One bubble, three roles. It was inside the studio's own stylesheet when
   only the manager had a tour; the teacher and the family have one now, and
   three copies of a bubble become three bubbles the first time one is
   touched.

   It is a REGION, not a dialog. The page underneath stays usable and nothing
   is trapped, so the bubble must never cover what it is pointing at and must
   never take the whole screen.
   ========================================================================== */

.nx-tour {
  position: absolute;
  /* Above the expanded rail (45), below the sandbox footer (50). */
  z-index: 48;
  box-sizing: border-box;
  inline-size: fit-content;
  min-inline-size: 280px;
  max-inline-size: 420px;
  padding: 14px 16px 16px;
  border: 1px solid var(--nx-border, #ebe0dd);
  border-radius: var(--nx-radius, 8px);
  background: var(--nx-surface, #fff);
  box-shadow: 0 10px 34px rgb(45 35 43 / 14%);
  /* The progress line is drawn on the bottom edge, so nothing may spill. */
  overflow: hidden;
}
.nx-tour:focus-visible { outline: 2px solid var(--nx-accent, #94505d); outline-offset: 3px; }

/* Row one: whose view this is, and the way out. */
.nx-tour__top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-block-end: 8px;
}
.nx-tour__perspective {
  font: 700 11px/1.3 'Hanken Grotesk', system-ui, sans-serif;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--nx-muted, #6f6368);
  min-inline-size: 0;
}

.nx-tour__title {
  margin: 0 0 6px;
  font: 650 16px/1.35 'Hanken Grotesk', system-ui, sans-serif;
  letter-spacing: -0.01em;
  color: var(--nx-text, #4a3f42);
  max-inline-size: 34ch;
}
.nx-tour__body {
  margin: 0 0 14px;
  font-size: 13.5px; line-height: 1.6;
  color: var(--nx-muted, #6b5d61);
  max-inline-size: 46ch;
}

/* Row four: Next at the bottom left, the quiet ones after it. */
.nx-tour__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
.nx-tour__next {
  order: 0;
  min-height: 38px; padding: 8px 16px;
  border: 1px solid var(--nx-accent, #94505d);
  border-radius: 4px;
  background: var(--nx-accent, #94505d);
  color: #fff;
  font: 650 13px 'Hanken Grotesk', system-ui, sans-serif;
  cursor: pointer;
}
.nx-tour__next:hover { background: #7f4552; border-color: #7f4552; }
.nx-tour__quiet {
  padding: 4px 0; border: 0; background: transparent;
  color: var(--nx-muted, #6f6368);
  font: 500 12px 'Hanken Grotesk', system-ui, sans-serif;
  text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer; white-space: nowrap;
}
.nx-tour__quiet:hover { color: var(--nx-accent, #94505d); }
/* Skip belongs to the top row and sits at its trailing end. */
.nx-tour__skip { margin-inline-start: auto; flex: none; }
/* The closing offer takes the end of the action row, away from Next. */
.nx-tour__onward { margin-inline-start: auto; }

/* Progress, as a line along the bottom edge. Never a "2 of 7": the count is
   on the progressbar's aria-label, where it is read and not seen. */
.nx-tour__progress {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  block-size: 3px;
  background: var(--nx-border, #ebe0dd);
}
.nx-tour__progress-fill {
  display: block; block-size: 100%; inline-size: 0;
  background: var(--nx-accent, #94505d);
  transition: inline-size .28s cubic-bezier(.62, .03, .31, 1);
}

/* At phone width there is no "beside": the bubble docks to the foot of the
   screen and takes the width it is given, and the pointer stands down. It
   still never covers its target, because the target is scrolled to the
   middle of what is left above it. */
.nx-tour.is-docked {
  position: fixed;
  inset: auto 12px calc(var(--nx-sandbox-height, 0px) + 12px);
  inline-size: auto;
  max-inline-size: none;
  min-inline-size: 0;
  /* Never taller than the room above the footer. A bubble that overflows the
     viewport hides its own Next button, which is the one control the reader
     needs; the body scrolls inside instead and the actions stay put. */
  max-block-size: calc(100dvh - var(--nx-sandbox-height, 0px) - 24px);
  display: flex;
  flex-direction: column;
}
.nx-tour.is-docked .nx-tour__body {
  min-block-size: 0;
  overflow-y: auto;
  /* The panel's own bar rather than the operating system's grey slab. */
  scrollbar-width: thin;
  scrollbar-color: var(--nx-scroll-thumb, #c6a7ae) transparent;
}
.nx-tour.is-docked .nx-tour__actions { flex: none; }

/* What the step is about, while the step is about it. */
.is-tour-target {
  outline: 1.5px solid color-mix(in srgb, var(--nx-accent, #94505d) 45%, transparent);
  outline-offset: 5px;
  border-radius: 6px;
}

/* Tour pointers use dots; interaction and loading lines keep their waves. */
.nx-tour-pointer {
  position: absolute; inset: 0;
  inline-size: 100%; block-size: 100%;
  pointer-events: none;
  z-index: 47;
  overflow: visible;
}
.nx-tour-pointer[hidden] { display: none !important; }
.nx-tour-pointer__line {
  fill: none;
  stroke: var(--nx-accent, #94505d);
  stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 0 6;
}
.nx-tour-pointer__line.is-second { opacity: .38; stroke-width: 1.8; }
.nx-tour-pointer__node { fill: var(--nx-accent, #94505d); stroke: none; }
.nx-tour-pointer__node.is-second { opacity: .5; }
.nx-tour-pointer__ring {
  fill: var(--nx-accent, #94505d); fill-opacity: .16; stroke: none;
  transform-box: fill-box; transform-origin: 50% 50%;
}
.nx-tour-pointer__ring.is-second { fill-opacity: .09; }
@media (prefers-reduced-motion: no-preference) {
  .nx-tour-pointer__ring { animation: nx-tour-ring 2.6s ease-in-out infinite; }
}
@keyframes nx-tour-ring {
  0%, 100% { transform: scale(1); fill-opacity: .16; }
  50% { transform: scale(1.35); fill-opacity: .05; }
}
/* Reduced motion: the line and the fill arrive, they do not travel. */
@media (prefers-reduced-motion: reduce) {
  .nx-tour__progress-fill { transition: none; }
}

/* The quiet replay in the sandbox footer. */
.nx-tour-replay {
  padding: 4px 0; border: 0; background: transparent;
  color: var(--rose-deep, #94505d);
  font: 600 12px 'Hanken Grotesk', system-ui, sans-serif;
  text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer; white-space: nowrap;
}

/* The "YOUR LOGO HERE" placeholder is GONE, and with it its CSS. A studio
   that has not uploaded a logo has no logo, and a dashed box saying so is a
   piece of unfinished furniture sitting in the masthead of every screen. The
   header shows the Nixie wordmark and the studio's name; when a studio has a
   logo stored, the surface script puts it in beside them and nothing is drawn
   in its place when there is none. */


/* ==========================================================================
   ONE SHELL FOR THE STUDIO WORKSPACE — the chrome's geometry, stated once

   The timetable, the desk and Backstage are three screens of one workspace.
   Each used to state its own surround, its own sheet box, its own header and
   its own rail in its own stylesheet, loaded after this one and so winning at
   equal specificity. The three never agreed. Measured at 1440x900 the sheet
   was [29,18,1382,786] on the timetable, [29,18,1382,785] on the desk and —
   once Backstage's stylesheet was published at all — [36,23,1368,855] there,
   with the header 81px on one screen and 63px on the next. That is what the
   owner was describing: "when I go to Payments something resizes".

   THE TIMETABLE IS THE REFERENCE, because it is the screen the owner said he
   liked. Its numbers are the ones below. The desk and Backstage were given
   them; they were not asked to compromise.

   The rules are keyed on SHARED class names — .studio-shell on the body,
   .nx-sheet, .nx-sheet-header, .nx-sheet-layout — which all three surfaces
   now carry alongside their own names. The per-surface geometry that used to
   fight this has been DELETED from studio.css, management.css and
   backstage.css rather than overridden, so there is nothing left to out-rank
   and no !important anywhere in this block.
   ========================================================================== */

html, body { margin: 0; }

/* The skip link is taken OUT OF FLOW here, in the shared sheet that every
   studio surface loads, rather than in each surface's own stylesheet.

   It matters which file says it. The surface sheets are added by script on a
   client-side move and are not in force for the first frame after the markup
   arrives, so Backstage's "Skip to Backstage" — `position:absolute` in
   backstage.css — was briefly a 21px inline element sitting in flow ABOVE the
   sheet and pushing the whole window down: header and rail 21px lower than on
   every other screen for one frame. This file is already applied, so the link
   is out of flow from the first frame and the chrome never moves.

   A skip link is the first thing in the body and the last thing that should
   ever affect layout; it is only on screen when it has focus. */
.studio-shell .skip,
.studio-shell .skip-link {
  position: absolute;
  inset-inline-start: -9999px;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
}
.studio-shell .skip:focus,
.studio-shell .skip-link:focus {
  inset-inline-start: 8px;
  inset-block-start: 8px;
  z-index: 2147483001;
  inline-size: auto;
  block-size: auto;
  padding: 10px 14px;
  border-radius: 6px;
  background: var(--paper, #fdfcf9);
  color: var(--ink, #30302e);
}

/* ----------------------------------------------------------- the surround
   The ground the window sits on. The dark wash this used to be read as a
   heavy banner; the home page shows the product in a window on its own warm
   paper, and this is that ground, so the sandbox and the marketing page are
   recognisably the same drawing. */
/* The ROOT is constrained as well as the body.

   `overflow:hidden` on the body propagates to the viewport — but only once
   the body's own rule is in force. For the first frame after a client-side
   move the surface stylesheets are not yet applied, so <html> was briefly
   `overflow-y: visible` and scrollable, and a scrollbar gutter appeared and
   then went: the sheet measured 1382x788 for a frame and 1380x786 after it.
   Two pixels, on four of eleven destinations, and the verifier was right to
   call it a move. Saying it here, in the sheet that is always applied, means
   the gutter never exists. */
html:has(body.studio-shell) {
  overflow: hidden;
  block-size: 100%;
  /* And the gutter is never reserved, whatever is momentarily in the
     document. During a swap the route holds a still picture of the outgoing
     page over the live one, so for a few frames there are two copies of a
     surface in the document and the taller of them can ask for a scrollbar.
     The 2px gutter that appears and goes is a real change in the sheet's
     width — 1382 for a frame, then 1380 — on four of eleven destinations.
     stable would RESERVE the gutter; none is right here, because the page
     genuinely never scrolls: the sheet does, inside itself. */
  scrollbar-width: none;
}
html:has(body.studio-shell)::-webkit-scrollbar { display: none; }

body.studio-shell {
  /* An explicit viewport height, not 100%, which only resolves if every
     ancestor has one. Backstage's own `html,body{height:100%}` was deleted
     with the rest of its geometry and the sheet's 96dvh then measured against
     a box 940px tall instead of the viewport — 864px of sheet against the
     timetable's 786. The height belongs with the rest of the shared chrome. */
  block-size: 100dvh;
  background: var(--linen, #f0eee8);
  padding: 2vh 2vw;
  overflow: hidden;
}
/* Room for the sandbox bar fixed along the bottom, so the sheet sits above it
   rather than under it. */
body.studio-shell.is-demo-studio { padding-block-end: calc(2vh + 40px); }

/* ---------------------------------------------------------------- the sheet
   One box on every screen. The height is fixed rather than fitted to content:
   a window that changes size when you change page is a window that makes you
   re-find everything. */
.nx-sheet {
  inline-size: 96vw;
  block-size: 96dvh;
  margin-inline: auto;
  background: var(--cream, #fdfcf9);
  border: 1px solid #ded8cf;
  /* The sheet's own box model, stated here rather than inherited from
     `.nx *` in app/nixie.css. That rule is in a DIFFERENT stylesheet, which
     is not in force for the first frame after a client-side move — so the
     sheet was briefly content-box and its 1px border was drawn OUTSIDE its
     1382.39px width: 1384 for a frame, then 1382. Two pixels, and the
     verifier was right to call it a move. */
  box-sizing: border-box;
  border-radius: 14px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: 0 18px 44px #58423b1f, 0 3px 10px #58423b12;
}
@media (min-width: 1500px) {
  /* Past this width the sheet stops growing: a timetable stretched across a
     very wide monitor is harder to read across, not easier. */
  .nx-sheet { inline-size: min(97vw, 1900px); }
}
@media (max-width: 760px) {
  body.studio-shell { padding: 1.5dvh 1.5vw; }
  .nx-sheet { inline-size: 97vw; border-radius: 10px; }
}

/* --------------------------------------------------------------- the header
   ONE header on every screen, so it does not change height underneath the
   reader. 81px on the timetable against 63px on the desk was the jump the
   owner could see; both are this now. */
.nx-sheet-header {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 18px 26px;
  background: var(--paper, #fdfcf9);
  border-bottom: 1px solid var(--line, #e7e5df);
  flex: none;
}
@media (max-width: 760px) {
  .nx-sheet-header { padding: 14px 16px; gap: 12px; }
}

/* The header's CONTENTS, so the three screens do not merely share a box but
   put the same things in the same places. The desk drew a 111px wordmark and
   a one-line name where the timetable drew a 74px one and a two-line identity
   block, so the same header was 81px on one screen and 61px on the next even
   once its padding was shared. Same elements, same order, same sizes now:
   wordmark, the studio's name with the reader's role under it, actions,
   and the save state at the end. */
.nx-sheet-header .sheet-brand { display: flex; flex: none; }
.nx-sheet-header .sheet-brand img { display: block; inline-size: 74px; block-size: auto; filter: grayscale(1); }
.nx-sheet-header .studio-identity { min-inline-size: 0; }
/* When the tour points at the studio and the reader, the box hugs those two lines. */
.nx-sheet-header .studio-identity.is-tour-target { flex: 0 1 auto; inline-size: fit-content; max-inline-size: 100%; }
.nx-sheet-header .studio-identity h1 { font-size: 17px; font-weight: 750; margin: 0 0 2px; letter-spacing: -.015em; line-height: 1.25; }
/* The role line keeps its height even before the session names the reader,
   and on a screen that never names one. An empty span collapsed, so the
   header stood 76px on the timetable and 72px everywhere else — the same
   header, two heights, which is the jump the owner saw. min-block-size is the
   LINE's height, not the font's: 1em left it 4px short. */
.nx-sheet-header .studio-identity span { display: block; font-size: 12px; color: var(--muted, #68665f); line-height: 1.3; min-block-size: 1.3em; }
/* Actions stay in the upper-right corner of the shared header. */
.nx-sheet-header .sheet-links { display: flex; gap: 16px; margin-inline-start: auto; font-size: 12px; flex: none; }
.nx-sheet-header .sheet-links a { color: var(--ink-soft, #57554f); text-decoration: none; }
.nx-sheet-header .sheet-links a:hover { color: var(--ink, #30302e); text-decoration: underline; text-underline-offset: 3px; }
.nx-sheet-header .save-state { flex: none; margin-inline-start: 0; color: var(--muted, #68665f); font-size: 11px; white-space: nowrap; min-inline-size: 0; }
/* The header NEVER wraps. At 390px each screen wrapped at a different point —
   the desk's name is longer than Backstage's, the timetable's role line
   longer again — so the same header came out 68, 78 and 97 pixels tall on
   three screens of one workspace. One row, and the name takes what is left
   after the fixed parts and ellipses rather than pushing anything onto a
   second line. */
.nx-sheet-header { flex-wrap: nowrap; }
.nx-sheet-header .studio-identity { flex: 1 1 auto; min-inline-size: 0; }
.nx-sheet-header .studio-identity h1,
.nx-sheet-header .studio-identity span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 760px) {
  .nx-sheet-header .sheet-links { gap: 12px; font-size: 11px; }
  /* The save state is the first thing to go: it is a status, and the reader
     can see the studio is saving from the screen itself. */
  .nx-sheet-header .save-state { display: none; }
}
@media (max-width: 450px) {
  .nx-sheet-header .sheet-brand { display: none; }
}

/* --------------------------------------------------------------- the layout
   The rail is positioned inside this, so it establishes the containing block
   for it on all three screens. */
.nx-sheet-layout {
  position: relative;
  display: flex;
  flex: 1;
  min-block-size: 0;
}

/* ----------------------------------------------------------------- the rail
   One rail: the same 56px column over the left edge of the work area, opening
   to 196px. It used to be an absolute overlay on two screens and a flex column
   that widened its ROWS on the third. */
.nx-sheet-layout > .nx-rail {
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  z-index: 8;
  inline-size: var(--nx-rail-closed, 56px);
  overflow: hidden;
  transition: inline-size 150ms ease-out;
  background: transparent;
  border-inline-end: 1px solid transparent;
  /* Stated here, not inherited from `.nx *` in app/nixie.css. The body's
     classes are rewritten as one surface hands over to the next, so for a
     frame during a move the rail can be outside `.nx` — and then it is
     content-box, and the 1px border an expanded rail draws lands OUTSIDE its
     width: 212 becomes 213 for one frame, which the step verifier reports as
     CHROME MOVED. The same first-frame trap as .nx-sheet's own box-sizing;
     see the note in docs/verify-rail.md. A rule the chrome depends on belongs
     on the chrome. */
  box-sizing: border-box;
}
/* An open menu stays above workspace content; tour cards sit above it (48). */
.nx-sheet-layout > .nx-rail.is-expanded {
  z-index: 45;
  inline-size: var(--nx-rail-open, 196px);
  background: var(--paper, #fdfcf9);
  border-inline-end: 1px solid var(--line, #e7e5df);
  box-shadow: 5px 0 14px rgb(48 48 46 / 5%);
}
.nx-rail .rail-toggle, .nx-rail .rail-link {
  display: flex;
  align-items: center;
  gap: 16px;
  inline-size: 100%;
  min-block-size: 49px;
  padding: 13px 17px;
  border: 0;
  background: transparent;
  color: var(--ink, #30302e);
  text-decoration: none;
  white-space: nowrap;
  font: 500 12px 'Hanken Grotesk', sans-serif;
  cursor: pointer;
  text-align: left;
  box-sizing: border-box;
}
.nx-rail .rail-toggle { min-block-size: 51px; border-block-end: 1px solid var(--line, #e7e5df); font-size: 11px; }
/* Every row's icon holds its size in the icon column; a flex item that may
   shrink collapses to nothing when the label beside it is wide, which is how
   Backstage lost all but one of its icons. */
.nx-rail .rail-toggle svg, .nx-rail .rail-link svg { flex: none; }
.nx-rail .rail-toggle:hover { background: var(--linen, #f0eee8); }
.nx-rail .rail-divider { margin: 8px 14px; border: 0; border-block-start: 1px solid var(--line, #e7e5df); }

/* A row's LABEL exists only once the rail is open. Collapsed there are 56
   pixels and the icon, so a label would simply lie across the work beside it
   — which is what Backstage did, because the rule that hid them was in the
   per-surface geometry that has now gone. It belongs with the rail. */
.nx-rail .rail-link span, .nx-rail .rail-toggle span {
  opacity: 0;
  transform: translateX(-8px);
  transition: transform 180ms cubic-bezier(.16,1,.3,1), opacity 120ms ease;
}
.nx-rail.is-expanded .rail-link span,
.nx-rail.is-expanded .rail-toggle span,
.nx-rail.is-pinned .rail-link span,
.nx-rail.is-pinned .rail-toggle span { opacity: 1; transform: translateX(0); }
@media (prefers-reduced-motion: reduce) {
  .nx-rail .rail-link span, .nx-rail .rail-toggle span { transform: none; transition: none; }
}
/* The collapsed rail floats over the work area's leading edge, so the content
   starts clear of the icons rather than under them. The timetable insets its
   own rows; the desk and Backstage inset the whole region. */
.nx-sheet-layout > main,
.nx-sheet-layout > .backstage-workspace { padding-inline-start: 82px; }
@media (max-width: 760px) {
  .nx-sheet-layout > main,
  .nx-sheet-layout > .backstage-workspace { padding-inline-start: 60px; }
}
@media (max-width: 760px) {
  .nx-sheet-layout > .nx-rail { inline-size: var(--nx-rail-closed); }
  .nx-sheet-layout > .nx-rail.is-expanded { inline-size: var(--nx-rail-open); }
  .nx-rail .rail-toggle, .nx-rail .rail-link { padding: 12px 13px; min-block-size: 46px; }
}

/* ------------------------------------------------------- the content region
   The one thing a surface still owns: what is INSIDE it changes from screen to
   screen, the box does not. The collapsed rail floats over its left edge, so
   the rows whose content would sit under the icons are inset rather than the
   whole region being pushed across. */
.nx-sheet-layout > .studio-workspace,
.nx-sheet-layout > main,
.nx-sheet-layout > .backstage-workspace {
  flex: 1;
  min-inline-size: 0;
  min-block-size: 0;
  overflow: auto;
}

/* ============================================================ the window
   The demo is shown inside a drawing of a browser window. It is drawn to the
   same design as the one on the home page (.tilt-window in hero-flow.css) —
   ONE toolbar row, three muted dots, back/forward/reload, a long rounded
   address pill with a padlock — because the sandbox and the marketing page
   are showing the same product and a reader should recognise the second from
   the first.

   What went: the tab strip, which made this the only window in the product
   with two rows of chrome; "Australian studio software", which is marketing
   copy inside a drawing of a browser; and the red traffic light doubling as a
   close control, which is not what that dot does on a Mac and meant the way
   out was hidden in decoration. The way out is now one quiet x at the
   right-hand end of the row, where a window's close control is.

   HONESTY, unchanged: the frame states nothing about the connection. The
   padlock and the navigation glyphs are inert and aria-hidden, the pill reads
   an address rather than a scheme, and no part of this is offered as a
   security indicator. The x is the only real control, and it leaves. */
.nx-window-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  flex: none;
  padding: 11px 16px;
  background: var(--linen, #f0eee8);
  border-block-end: 1px solid #e3ddd3;
  font-size: 11.5px;
  /* Its own line-height, not the surface's. body.manager sets 1.55 against
     the timetable's, which made the same bar 51px on the desk and 48px on the
     timetable and pushed everything under it down by three pixels. */
  line-height: 1.5;
  block-size: 48px;
  box-sizing: border-box;
  color: var(--muted, #68665f);
}
.nx-window-dots { display: flex; gap: 7px; flex: none; }
.nx-window-dots i { inline-size: 8px; block-size: 8px; border-radius: 50%; background: #c9bcae; }
/* The browser controls use the real tab history and reload this preview. */
.nx-window-nav { display: flex; gap: 2px; margin-inline-start: 6px; flex: none; }
.nx-window-nav button {
  display: inline-grid; place-items: center;
  inline-size: 28px; block-size: 28px; padding: 0;
  border: 0; border-radius: 5px; background: transparent;
  color: var(--muted, #68665f); cursor: pointer;
}
.nx-window-nav button:hover { background: #e3ddd3; color: var(--ink, #30302e); }
.nx-window-nav button:focus-visible { outline: 2px solid var(--rose-deep, #94505d); outline-offset: 1px; }
.nx-window-nav svg { inline-size: 15px; block-size: 15px; }
.nx-window-address {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-inline-size: 0;
  margin-inline: 6px;
  padding: 4px 11px;
  background: var(--paper, #fffefa);
  border: 1px solid #e6ded4;
  border-radius: 11px;
  letter-spacing: .01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nx-window-address svg { inline-size: 11px; block-size: 11px; flex: none; color: var(--sage-deep, #4a6553); }
.nx-window-address [data-studio-address] { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; }
/* The way out, at the right-hand end of the row where a window's close is. A
   24px target, quiet until it is wanted. */
.nx-window-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: 24px;
  block-size: 24px;
  border-radius: 5px;
  color: var(--muted, #68665f);
  text-decoration: none;
  border: 0; background: transparent; padding: 0; cursor: pointer;
}
.nx-window-close svg { inline-size: 13px; block-size: 13px; }
.nx-window-close:hover { background: #e3ddd3; color: var(--ink, #30302e); }
.nx-window-close:focus-visible { outline: 2px solid var(--rose-deep, #94505d); outline-offset: 1px; }
@media (max-width: 560px) {
  /* On a phone the decorative dots yield room to usable navigation. */
  .nx-window-dots { display: none; }
  .nx-window-nav { margin-inline-start: 0; }
  .nx-window-nav button { inline-size: 36px; block-size: 36px; }
  .nx-window-bar { padding-inline: 10px; }
}

/* A real studio is the software itself and wears none of this. */
body.is-real-studio .nx-window-bar { display: none; }

/* The one rail row that leaves this studio. Everything above it moves around
   inside the studio being read; this one goes to the list of studios the
   person belongs to, so a rule separates it from the operations rather than
   letting it read as another view of the same place. */
.nx-rail .rail-link--across {
  margin-block-start: 10px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--nx-line, #ebe0dd);
}

/* Save feedback lives with the action; the header stays quiet. */
.nx-sheet-header .save-state[hidden] { display: none; }

/* Fixed inner geometry: hovering reveals labels without moving any icon or
   adding height for the pin. Pinning reserves a real column in the flex row. */
.nx-sheet-layout {
  --nx-rail-closed: 56px;
  --nx-rail-open: 196px;
  --nx-rail-heading: 52px;
  --nx-workspace-gutter: 24px;
  --nx-workspace-inset: calc(var(--nx-rail-closed) + var(--nx-workspace-gutter));
}
.nx-sheet-layout:has(> .nx-rail.is-pinned) { --nx-workspace-inset: var(--nx-workspace-gutter); }
.nx-sheet-layout > .nx-rail.is-pinned {
  position: relative;
  inset: auto;
  flex: 0 0 var(--nx-rail-open);
  inline-size: var(--nx-rail-open);
  box-shadow: none;
  transition: none;
}
.nx-sheet-layout > .nx-rail .rail-toggle {
  inline-size: calc(var(--nx-rail-open) - 1px);
  block-size: var(--nx-rail-heading);
  min-block-size: var(--nx-rail-heading);
  padding-block: 0;
  line-height: 1;
}
.nx-sheet-layout > .nx-rail .rail-toggle .rail-heading { display: block; }
.nx-sheet-layout > .nx-rail .rail-toggle .rail-pin {
  display: inline-flex;
  position: absolute;
  inset-inline-end: 10px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: 28px;
  block-size: 28px;
  min-block-size: 0;
  margin: 0;
  padding: 5px;
  align-items: center;
  justify-content: center;
}
/* Reserve the pin's space, but reveal it only on its own row or focus. */
.nx-sheet-layout > .nx-rail .rail-toggle .rail-pin { opacity:0;pointer-events:none; }
.nx-sheet-layout > .nx-rail .rail-toggle:hover .rail-pin,
.nx-sheet-layout > .nx-rail .rail-toggle:focus-within .rail-pin,
.nx-sheet-layout > .nx-rail .rail-toggle .rail-pin[data-menu-open] { opacity:1;pointer-events:auto; }
.nx-pin-menu { position:fixed;inset:auto;margin:0;padding:6px;border:1px solid var(--line);border-radius:6px;background:var(--paper);color:var(--ink);box-shadow:var(--shadow-md);font:400 12px/1.4 'Hanken Grotesk',sans-serif;max-inline-size:calc(100vw - 16px); }
.nx-pin-menu[hidden] { display:none; }
.nx-pin-menu button { display:block;padding:8px 12px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer; }
.nx-pin-menu button:hover { background:var(--rose-wash); }
.nx-pin-menu button:focus-visible { outline:2px solid var(--rose-deep);outline-offset:-2px; }
.nx-sheet-layout > .nx-rail > nav {
  inline-size: calc(var(--nx-rail-open) - 1px);
  block-size: calc(100% - var(--nx-rail-heading));
  overflow: hidden auto;
  /* A gutter narrowed every highlight. Native scrolling and focused-link
     scrolling remain; the rows occupy the complete navigation width. */
  scrollbar-width: none;
  scrollbar-gutter: auto;
  overscroll-behavior: contain;
}
.nx-sheet-layout > .nx-rail .rail-link span,
.nx-sheet-layout > .nx-rail .rail-toggle span {
  transform: none;
  transition: opacity 100ms ease;
}
.nx-sheet-layout > .nx-rail:not(.is-expanded):not(.is-pinned) .rail-toggle .rail-heading { opacity: 0; }
/* The content wrapper is display:contents, so each screen is still a flex
   item. These selectors follow the actual DOM rather than a former sibling. */
.nx-sheet-layout > .studio-region > :is(main,.studio-workspace,.backstage-workspace,.notebook-workspace) {
  flex: 1 1 0%;
  min-inline-size: 0;
  min-block-size: 0;
}
@media (max-width: 760px) {
  .nx-sheet-layout { --nx-rail-closed: 48px; --nx-rail-open: 184px; --nx-rail-heading: 48px; --nx-workspace-gutter: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-sheet-layout > .nx-rail,
  .nx-sheet-layout > .nx-rail .rail-link span,
  .nx-sheet-layout > .nx-rail .rail-toggle span { transition: none; }
}

.nx-sheet-layout > .nx-rail:focus-within { transition: none; }

/* Operations is the home destination; pin remains a separate control. */
.nx-sheet-layout > .nx-rail .rail-home {
  display: flex;
  align-items: center;
  gap: 16px;
  align-self: stretch;
  flex: 1;
  min-inline-size: 0;
  margin-inline: -17px;
  padding-inline: 17px 40px;
  color: inherit;
  text-decoration: none;
  box-sizing: border-box;
}
.nx-sheet-layout > .nx-rail .rail-home:hover { background: var(--rose-faint); }
.nx-sheet-layout > .nx-rail .rail-home[aria-current="page"] { background: var(--rose-wash); color: var(--rose-deep); font-weight:700; box-shadow:inset 3px 0 0 var(--rose-deep); }
.nx-sheet-layout > .nx-rail .rail-home[aria-current="page"] .rail-heading { color: var(--rose-deep); }
.nx-sheet-layout > .nx-rail .rail-home:focus-visible { outline: 2px solid var(--rose-deep); outline-offset: -3px; }
@media (max-width: 760px) {
  .nx-sheet-layout > .nx-rail .rail-home { margin-inline: -13px; padding-inline-start: 13px; }
}

/* Settings remains reachable below the scrollable Operations destinations. */
/* Reserve the pin's space, but reveal it only on its own row or focus. */
.nx-sheet-layout > .nx-rail .rail-toggle .rail-pin { opacity:0;pointer-events:none; }
.nx-sheet-layout > .nx-rail .rail-toggle:hover .rail-pin,
.nx-sheet-layout > .nx-rail .rail-toggle:focus-within .rail-pin,
.nx-sheet-layout > .nx-rail .rail-toggle .rail-pin[data-menu-open] { opacity:1;pointer-events:auto; }
.nx-pin-menu { position:fixed;inset:auto;margin:0;padding:6px;border:1px solid var(--line);border-radius:6px;background:var(--paper);color:var(--ink);box-shadow:var(--shadow-md);font:400 12px/1.4 'Hanken Grotesk',sans-serif;max-inline-size:calc(100vw - 16px); }
.nx-pin-menu[hidden] { display:none; }
.nx-pin-menu button { display:block;padding:8px 12px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer; }
.nx-pin-menu button:hover { background:var(--rose-wash); }
.nx-pin-menu button:focus-visible { outline:2px solid var(--rose-deep);outline-offset:-2px; }
.nx-sheet-layout > .nx-rail > nav { block-size: calc(100% - var(--nx-rail-heading) - 52px); }
.nx-sheet-layout > .nx-rail > .rail-settings { position:absolute;inset-block-end:0;inset-inline-start:0;inline-size:calc(var(--nx-rail-open) - 1px);block-size:52px;box-sizing:border-box;border-block-start:1px solid var(--line); }

.nx-sheet-layout > .nx-rail > nav::-webkit-scrollbar { display:none; }
.nx-sheet-layout > .nx-rail :is(.rail-link,.rail-home):focus-visible { outline:2px solid var(--rose-deep);outline-offset:-3px; }
@media(forced-colors:active){
 .nx-sheet-layout > .nx-rail :is(.rail-link,.rail-home)[aria-current="page"] { outline:2px solid Highlight;outline-offset:-3px; }
}

/* Phones use an explicit disclosure and overlay, preserving the desktop pin
   preference without squeezing a 320px workspace into the remaining column. */
.studio-menu-toggle { display:none; }
@media(max-width:600px) {
 .nx-sheet-header { display:grid;grid-template-columns:44px minmax(0,1fr) auto;gap:0 8px;padding:8px 12px; }
 .nx-sheet-header .sheet-brand { display:none; }
 .nx-sheet-header .studio-menu-toggle { display:grid;place-items:center;grid-row:1;grid-column:1;inline-size:44px;block-size:44px;padding:0;border:1px solid var(--line);border-radius:6px;background:var(--paper);color:var(--ink);cursor:pointer; }
 .nx-sheet-header .studio-identity { grid-column:2 / -1;grid-row:1; }
 .nx-sheet-header .studio-identity h1 { max-width:none;font-size:16px; }
 .nx-sheet-header .studio-identity span { max-width:none;font-size:12px; }
 .nx-sheet-header .sheet-links { grid-column:1 / 3;grid-row:2;margin:0;gap:16px;font-size:12px; }
 .nx-sheet-header .sheet-links a { display:inline-flex;align-items:center;min-height:36px; }
 .nx-sheet-header .studio-guide { grid-row:2;grid-column:3; }
 .studio-menu-toggle:focus-visible { outline:2px solid var(--rose-deep);outline-offset:2px; }
 .nx-sheet-layout,.nx-sheet-layout:has(> .nx-rail.is-pinned) { --nx-workspace-inset:12px; --nx-rail-open:min(280px,calc(100vw - 40px)); }
 .nx-sheet-layout > .nx-rail,.nx-sheet-layout > .nx-rail.is-pinned { position:absolute;inset:0 auto 0 0;flex:none;inline-size:0;visibility:hidden;transition:none; }
 .nx-sheet-layout > .nx-rail.is-expanded { inline-size:var(--nx-rail-open);visibility:visible;box-shadow:8px 0 24px color-mix(in srgb,var(--ink) 12%,transparent); }
 .nx-sheet-layout > .nx-rail .rail-toggle .rail-pin { display:none; }
}

/* Staff and Settings are utility workspaces: a quiet, always-open local menu.
   The studio's hover/pin preference is retained for Back to Nixy. */
.nx-sheet-layout.utility-layout{--nx-workspace-inset:24px}
.nx-sheet-layout.utility-layout>.nx-rail.utility-rail{position:relative;inset:auto;flex:0 0 220px;inline-size:220px;visibility:visible;overflow:hidden;background:var(--ink);color:var(--paper);border:0;box-shadow:none;transition:none;z-index:45}
.utility-layout .staff-views{display:none}
/* Explicitly override the old shell rule in already-cached base stylesheets. */
.utility-layout .staff-inquiry-tabs{display:flex}
@media(max-width:600px){
 .nx-sheet-layout.utility-layout{--nx-workspace-inset:12px}
 .nx-sheet-layout.utility-layout>.nx-rail.utility-rail{position:absolute;inset:0 auto 0 0;flex:none;inline-size:min(280px,calc(100vw - 40px));visibility:hidden;transform:translateX(-100%)}
 .nx-sheet-layout.utility-layout>.nx-rail.utility-rail.utility-open{visibility:visible;transform:translateX(0);box-shadow:8px 0 22px color-mix(in srgb,var(--ink) 16%,transparent)}
}

.studio-self-profile{font-size:12px;color:var(--rose-deep);margin-inline-start:14px;text-underline-offset:3px}
@media(max-width:600px){
 .nx-sheet-header .studio-self-profile{grid-row:2;grid-column:3;margin-inline-start:0;min-height:36px;display:flex;align-items:center;justify-self:end;white-space:nowrap}
 .nx-sheet-header:has(.studio-self-profile) .studio-guide{grid-row:3;grid-column:1 / -1;justify-self:end}
}

/* Time is an orientation cue, never an alert. Shared by real timetables and
   public previews; the renderer supplies the minute-accurate vertical offset. */
:root { --nx-time-ink:#a34f5d; --nx-time-line:#ca939a; --nx-time-ground:#fff5f5; }
.nx-time-marker { position:absolute; inset-inline:0; top:var(--nx-time-offset); display:flex; align-items:center; z-index:4; pointer-events:none; transform:translateY(-50%); }
.nx-time-marker__label { flex:none; padding:3px 4px; min-width:42px; box-sizing:border-box; border:1px solid var(--nx-time-line); border-radius:2px; background:var(--nx-time-ground); color:var(--nx-time-ink); font:600 10px/1.2 'Hanken Grotesk',sans-serif; white-space:nowrap; }
.nx-time-marker__line { height:1px; background:var(--nx-time-line); flex:1; }
.nx-time-marker__context { position:absolute; inset-inline-end:4px; bottom:3px; font:500 10px/1.2 'Hanken Grotesk',sans-serif; background:var(--nx-surface); color:var(--nx-time-ink); padding:2px 4px; }
