/**
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * OFE application shell: top app bar, collapsible navigation rail and the
 * page frame that templates/ofe/base.html renders every page into.
 *
 * Every rule is scoped under `.ofe`, the class on <body> in ofe/base.html, so
 * this sheet cannot reach pages that still extend base_generic.html.
 *
 * The rail state lives on <html> as data-rail-state, set by js/ofe/shell.js
 * from the head of the page before <body> is parsed, so a remembered
 * collapsed rail never flashes open. The Advanced settings state lives on
 * <body> as data-advanced, also set by shell.js.
 *
 * Nothing below the app bar assumes its height. shell.js measures the bar
 * whenever its size changes and publishes the result as --ofe-bar-height on
 * <body>; every sticky offset and viewport-height calc reads that. Without
 * JavaScript nothing can be measured, and the bar may be taller than its
 * minimum (the account actions are shown in it, in place of the menu), so
 * the bar and the rail are not sticky then: they stack and scroll with the
 * page, and nothing can overlap.
 *
 * Requires, in order: tokens.css, this file, components.css.
 */

/* ------------------------------------------------------------------ base --- */

.ofe {
  /*
   * Shell geometry. The rail widths are the two states of the collapse; the
   * open rail narrows with the window down to the width its longest label
   * needs.
   */
  --ofe-rail-width: clamp(12rem, 20vw, 15rem);
  --ofe-rail-width-collapsed: 3.5rem;

  /*
   * The bar's own minimum height, and its real height. The real height is
   * replaced by shell.js with the measured value. The bar's min-height reads
   * only the minimum, never the measurement, so a bar that grew (a larger
   * font, say) shrinks back when it can.
   */
  --ofe-bar-min-height: 3.5rem;
  --ofe-bar-height: var(--ofe-bar-min-height);

  /*
   * The content pane. Every page is capped at this and centred, so no page
   * stretches a control across a very wide window.
   */
  --ofe-page-measure: 84.5rem;

  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface);
  font-family: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 0.9375rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.ofe *,
.ofe *::before,
.ofe *::after {
  box-sizing: border-box;
}

.ofe a { color: var(--md-sys-color-primary); }

/* Anything that could blow out a grid or flex track. */
.ofe .min0 { min-width: 0; }

/* Skip link: first thing in the tab order. */
.ofe .skip-link {
  position: absolute;
  left: 0.5rem;
  top: -3rem;
  z-index: 100;
  padding: 0.5rem 1rem;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  transition: top var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}

.ofe .skip-link:focus { top: 0.5rem; }

/* Consistent, visible focus everywhere. Never removed, only restyled. */
.ofe :focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
  border-radius: var(--md-sys-shape-corner-extra-small);
}

/* On the primary-coloured rail the ring is drawn in the on-primary colour. */
.ofe .rail :focus-visible { outline-color: var(--md-sys-color-on-primary); }

/* The icon sprite in ofe/_icons.html takes no space. */
.ofe .ofe-icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* --------------------------------------------------------------- app bar --- */

/*
 * One row from 320px up. Instead of wrapping, the bar gives way in steps as
 * the window narrows: the spacing tightens, the brand shortens with an
 * ellipsis, and at the breakpoints in the
 * responsive section the account name and the Advanced switch's words leave
 * the screen while staying in the accessible names. Below 320px the page
 * shrinks with the window, and the bar may wrap (see the responsive section).
 */
.ofe .appbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: clamp(0.25rem, 1.5vw, 0.75rem);
  min-width: 0;
  min-height: var(--ofe-bar-min-height);
  padding: 0.375rem clamp(0.5rem, 2vw, 1rem);
  background: var(--md-sys-color-surface);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}

.ofe .appbar__brand {
  flex: 0 1 auto;
  min-width: 2.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  color: var(--md-sys-color-primary);
  text-decoration: none;
}

.ofe .appbar__spacer { flex: 1 1 0; min-width: 0; }

/*
 * The current page's name, after the brand. It keeps its full width and the
 * brand shortens instead; only a name longer than about a third of the
 * window is cut short.
 */
.ofe .appbar__title {
  flex: 0 0 auto;
  max-width: 35vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 1rem;
  font-weight: 600;
  color: var(--md-sys-color-on-surface);
}

/* The switch and the account menu keep their size; only text shortens. */
.ofe .appbar__advanced { flex: 0 0 auto; }
.ofe .appbar > .menu { flex: 0 1 auto; min-width: 2.5rem; }

/* ---------------------------------------------------------------- switch --- */

/*
 * An on/off switch: a native checkbox with role="switch", invisible but laid
 * over the drawn track so it still takes the clicks and the focus. The state
 * is also written out in words next to it, so it never depends on the knob's
 * position alone.
 */
.ofe .switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
  font-size: 0.875rem;
  white-space: nowrap;
}

.ofe .switch__input {
  position: absolute;
  left: 0;
  top: 50%;
  width: 3rem;
  height: 1.75rem;
  margin: 0;
  opacity: 0;
  transform: translateY(-50%);
  cursor: pointer;
  /* Above the track, which is positioned too, so the input takes the click. */
  z-index: 1;
}

.ofe .switch__track {
  position: relative;
  flex: 0 0 auto;
  width: 3rem;
  height: 1.75rem;
  border: 2px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-highest);
  transition: background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}

.ofe .switch__thumb {
  position: absolute;
  top: 50%;
  left: 0.25rem;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: var(--md-sys-color-outline);
  transform: translateY(-50%);
  transition: left var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
    background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
    width var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
    height var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}

.ofe .switch__input:checked + .switch__track {
  border-color: var(--md-sys-color-primary);
  background: var(--md-sys-color-primary);
}

.ofe .switch__input:checked + .switch__track .switch__thumb {
  left: 1.5rem;
  width: 1.5rem;
  height: 1.5rem;
  background: var(--md-sys-color-on-primary);
}

/* The input is invisible, so its focus ring is drawn on the track. */
.ofe .switch__input:focus-visible + .switch__track {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.ofe .switch__state {
  min-width: 3.25em;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--md-sys-color-on-surface-variant);
}

/* The Advanced settings switch only works with JavaScript; without it, it is not offered. */
:root:not(.js) .ofe .appbar__advanced { display: none; }

/*
 * Content that only the Advanced settings switch reveals. Hidden only when
 * JavaScript runs and the switch is off, so without JavaScript everything
 * stays reachable.
 */
:root.js .ofe:not([data-advanced="on"]) [data-advanced-only] { display: none; }

/* The account menu button: avatar plus the signed-in user's name. */
.ofe .appbar__account-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.5rem;
  padding: 0.25rem 0.75rem 0.25rem 0.25rem;
  border: 0;
  border-radius: var(--md-sys-shape-corner-full);
  background: none;
  color: var(--md-sys-color-on-surface);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
  max-width: 100%;
  /*
   * The visually hidden texts inside are absolutely positioned. Without a
   * containing block here they count towards the bar's overflow at their
   * static position and push the page 4px wide on a 320px screen.
   */
  position: relative;
  overflow: hidden;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.ofe .appbar__account-button:hover,
.ofe .appbar__account-button[aria-expanded="true"] {
  background: var(--md-sys-color-surface-container-high);
}

.ofe .appbar__avatar {
  display: block;
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
}

.ofe .appbar__account-name {
  min-width: 0;
  max-width: min(14rem, 25vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------ icon button --- */

.ofe .icon-button {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.ofe .icon-button:hover { background: var(--md-sys-color-surface-container-high); }

.ofe .icon-button svg {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
}

/*
 * Without JavaScript the rail toggle cannot work, so it is not offered, the
 * same as the Advanced switch; the brand moves up into its place.
 */
:root:not(.js) .ofe [data-rail-toggle] { display: none; }

/*
 * Without JavaScript the bar's height is unknown (see the top of this file),
 * so the bar and the rail stay in the flow instead of sticking. The rail then
 * stretches to the full height of the page, so its colour runs down the whole
 * left side of a long page rather than stopping after one window.
 */
:root:not(.js) .ofe .appbar,
:root:not(.js) .ofe .rail { position: static; }
:root:not(.js) .ofe .rail { max-height: none; align-self: stretch; }

/* ------------------------------------------------------------------ body --- */

.ofe .shell {
  display: grid;
  grid-template-columns: var(--ofe-rail-width) minmax(0, 1fr);
  align-items: start;
}

:root[data-rail-state="collapsed"] .ofe .shell {
  grid-template-columns: var(--ofe-rail-width-collapsed) minmax(0, 1fr);
}

/* ------------------------------------------------------------------ rail --- */

/*
 * Navigation: the same links as the sidebar of base_generic.html, as a rail
 * that collapses to icons.
 *
 * The rail fits the height below the bar without scrolling, in steps:
 *
 *   1. Tall window: the full spacing (about 842px of window at 16px text).
 *   2. Shorter: the spacing around the headings and at the ends shrinks with
 *      the space left over (--rail-slack), down to a quarter of a rem. The
 *      links stay 2.5rem (40px) tall throughout, a comfortable touch target.
 *   3. At 46.25rem (740px) and below: the heading texts leave the screen,
 *      as in the collapsed rail, and only a gap marks each section. The
 *      groups keep their names (aria-labelledby resolves to hidden text).
 *   4. Below 40.25rem (644px), when the 14 links at 40px no longer fit, the
 *      rail scrolls as a last resort, with a thin scrollbar.
 *
 * Open and collapsed rails follow the same steps. Everything is in rem, so
 * the steps follow the text size; the slack uses the measured bar height.
 */
.ofe .rail {
  /*
   * Height left over once the links, the heading texts and the smallest
   * spacing are placed: 35rem of links, 5.16rem of heading text, 2.38rem of
   * minimum spacing, and 0.125rem kept spare against rounding.
   */
  --rail-slack: calc(100vh - var(--ofe-bar-height) - 42.66rem);
  position: sticky;
  top: var(--ofe-bar-height);
  align-self: start;
  /*
   * Fill the space below the app bar exactly; the list scrolls itself on a
   * short screen. `.shell` aligns its items to the start, so without this the
   * rail would stop after its last link. The second pair uses the dynamic
   * viewport height, so on a phone the rail follows the browser's own bars
   * as they show and hide; browsers without dvh keep the first pair.
   */
  min-height: calc(100vh - var(--ofe-bar-height));
  max-height: calc(100vh - var(--ofe-bar-height));
  min-height: calc(100dvh - var(--ofe-bar-height));
  max-height: calc(100dvh - var(--ofe-bar-height));
  overflow-y: auto;
  overflow-x: hidden;
  /* Only reached in step 4; see above. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.5) transparent;
  /*
   * The slack is shared out in proportion to how far each space can grow
   * (6.625rem in all): 0.5rem at the top, 1.75rem at the bottom, and 0.75rem
   * above and 0.125rem below each of the five headings.
   */
  padding: clamp(0.25rem, calc(0.25rem + var(--rail-slack) * 0.0755), 0.75rem) 0.5rem
    clamp(0.25rem, calc(0.25rem + var(--rail-slack) * 0.2642), 2rem);
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}

@supports (height: 100dvh) {
  .ofe .rail { --rail-slack: calc(100dvh - var(--ofe-bar-height) - 42.66rem); }
}

.ofe .rail__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
 * A section heading: plain text, not a link and not focusable. It names its
 * group (role="group" + aria-labelledby in ofe/_nav.html).
 */
.ofe .rail__section {
  margin: clamp(0.25rem, calc(0.25rem + var(--rail-slack) * 0.1132), 1rem) 0
    clamp(0.125rem, calc(0.125rem + var(--rail-slack) * 0.0189), 0.25rem);
  padding: 0 0.75rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  line-height: 1.5;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0.7;
}

.ofe .rail__link {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--md-sys-shape-corner-full);
  color: inherit;
  text-decoration: none;
  font-size: 0.875rem;
  white-space: nowrap;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.ofe .rail__link:hover { background: rgba(255, 255, 255, 0.14); }

/*
 * Current page. Marked by background, weight and colour together, so it never
 * depends on colour alone.
 */
.ofe .rail__link[aria-current="page"] {
  background: var(--md-sys-color-on-primary);
  color: var(--md-sys-color-primary);
  font-weight: 600;
}

/* A 24dp icon slot holding an SVG on its own 24dp grid. */
.ofe .rail__icon {
  flex: 0 0 1.5rem;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
}

.ofe .rail__icon svg {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
}

/*
 * Collapsed rail. The label leaves the flow but the link keeps its accessible
 * name through aria-label, and the title attribute gives a hover hint.
 */
:root[data-rail-state="collapsed"] .ofe .rail__label { display: none; }

:root[data-rail-state="collapsed"] .ofe .rail__link {
  justify-content: center;
  padding: 0.5rem 0;
}

/* Collapsed, a heading's text goes and its space stays as the section gap. */
:root[data-rail-state="collapsed"] .ofe .rail__section-text { display: none; }
:root[data-rail-state="collapsed"] .ofe .rail__section { padding: 0; }

/*
 * Step 3 of the height fit (see .rail): the heading texts leave the screen
 * and each section is marked by a gap only, open or collapsed. The gap
 * grows with what is left once the 35rem of links, the 1.25rem of smallest
 * gaps and 0.5rem of padding are placed (0.125rem kept spare).
 */
@media (max-height: 46.25rem) {
  .ofe .rail {
    --rail-slack-tight: calc(100vh - var(--ofe-bar-height) - 36.875rem);
    padding-block: 0.25rem;
  }
  .ofe .rail__section-text { display: none; }
  .ofe .rail__section {
    padding: 0;
    margin: clamp(0.25rem, calc(0.25rem + var(--rail-slack-tight) * 0.1), 1rem) 0 0;
  }
}

@supports (height: 100dvh) {
  @media (max-height: 46.25rem) {
    .ofe .rail { --rail-slack-tight: calc(100dvh - var(--ofe-bar-height) - 36.875rem); }
  }
}

/* ------------------------------------------------------------------ main --- */

.ofe .main {
  min-width: 0;
  padding: 1.5rem clamp(1rem, 3vw, 2rem) 2.5rem;
  /*
   * `width: 100%` is load-bearing: `.main` is a grid item, and auto inline
   * margins on a grid item size it to fit its content. A definite width
   * restores the full column, max-width clamps it, and the margins centre it.
   */
  width: 100%;
  max-width: calc(var(--ofe-page-measure) + 2 * clamp(1rem, 3vw, 2rem));
  margin-inline: auto;
}

.ofe .main:focus { outline: none; }

.ofe .page-head { margin-bottom: 1.25rem; }

.ofe .page-head__title {
  margin: 0 0 0.25rem;
  /* 1.25rem on a phone up to 1.5rem from about 1000px. */
  font-size: clamp(1.25rem, 1rem + 1vw, 1.5rem);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.ofe .page-head__sub {
  margin: 0;
  max-width: 68ch;
  color: var(--md-sys-color-on-surface-variant);
}

.ofe .page-head__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(0.5rem, 2vw, 1rem);
  flex-wrap: wrap;
}

/* The title and the actions wrap onto separate lines rather than overflow. */
.ofe .page-head__row > * { min-width: 0; max-width: 100%; }

/*
 * A page that needs the full width (a log, a wide editor) opts out of the
 * page measure through the main_class block in ofe/base.html.
 */
.ofe .main--bleed { max-width: none; }

/*
 * Panes: cards side by side where there is room, one column where there is
 * not. Each pane is at least 22rem, or the full width if that is less.
 */
.ofe .panes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: 1rem;
  align-items: start;
  min-width: 0;
}

.ofe .panes > * { min-width: 0; }

.ofe .panes > .card { margin-bottom: 0; }

/* A section heading inside a page, below the page head. */
.ofe .section-title {
  margin: 2rem 0 0.25rem;
  font-size: 1rem;
  font-weight: 600;
}

/* ------------------------------------------------------------ responsive --- */

/* Below 900px the rail is icons only, whatever was remembered. */
@media (max-width: 900px) {
  .ofe { --ofe-rail-width: var(--ofe-rail-width-collapsed); }
  .ofe .rail__label,
  .ofe .rail__section-text { display: none; }
  .ofe .rail__link { justify-content: center; padding: 0.5rem 0; }
  .ofe .rail__section { padding: 0; }
}

/*
 * Below 720px the Advanced switch drops its words and keeps the switch. The
 * label is hidden from sight only, so the switch is still named "Advanced
 * settings" (aria-labelledby) and the label's title still gives a hover hint;
 * the state text is visual only (aria-hidden), and the switch itself reports
 * on or off.
 */
@media (max-width: 720px) {
  .ofe .appbar__advanced .switch__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
  .ofe .appbar__advanced .switch__state { display: none; }
}

@media (max-width: 600px) {
  .ofe .main { padding: 1rem clamp(0.5rem, 3vw, 1rem) 4rem; }
  /* Hidden from sight only: it is still part of the button's accessible name. */
  .ofe .appbar__account-name {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
  .ofe .appbar__account-button { padding-right: 0.25rem; }
}

/*
 * Below 320px the whole page shrinks with the window rather than breaking
 * words or scrolling sideways: the root text size follows the width (5vw is
 * 16px at 320px and 8px at 160px), and as every size here is in rem, the
 * text, the rail, the bar and the spacing shrink together. The query itself
 * is measured in the browser's own text size, not the root's, so it cannot
 * switch itself off; at 20rem 5vw equals that text size, so nothing jumps.
 *
 * As a safety net the bar may also wrap there. The page name goes, as the
 * rail marks the current section and every page has its own heading, and
 * the last item (the account menu, or Login when signed out) stays at the
 * right when it moves to a second row. shell.js measures the taller bar, so
 * the rail still sits under it.
 *
 * The focus ring stays a fixed 3px with a 2px gap, so the rail keeps at
 * least 5px at its sides: it clips sideways, and would cut the ring off.
 */
@media (max-width: 19.99rem) {
  :root { font-size: min(100%, 5vw); }
  .ofe .rail { padding-inline: max(0.5rem, 5px); }
  .ofe .appbar { flex-wrap: wrap; }
  .ofe .appbar__title { display: none; }
  .ofe .appbar > :last-child { margin-inline-start: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .ofe * { transition-duration: 1ms !important; }
}
