/* OpenBatterySim — component layer.
   Ports design-import/components/{core,navigation,surfaces,data}/*.jsx to plain CSS
   classes. Every value comes from tokens/*.css via var(); no raw hex, no accent hue,
   no shadow, no blur, no gradient. Interaction rules follow design-import/readme.md
   "Visual foundations" (States / Animation / Transparency & blur). */

/* ---------- core ---------- */

/* Button.jsx — ghost pill CTA. 18/24 padding, 32px radius, >=44px touch target. */
.obs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--button-pad-y) var(--button-pad-x);
  border-radius: var(--radius-pill);
  min-height: var(--touch-min);
  font-family: var(--font-ui);
  font-size: var(--button-cap-size);
  line-height: var(--button-cap-lh);
  letter-spacing: var(--button-cap-ls);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap; /* template puts white-space:nowrap on every button inline */
  box-shadow: var(--shadow-none);
  transition: opacity 160ms linear, background 160ms linear;
}
.obs-btn--ghost-dark {
  background: transparent;
  color: var(--on-primary);
  border: var(--hairline) solid var(--on-primary);
}
.obs-btn--ghost-light {
  background: transparent;
  color: var(--ink);
  border: var(--hairline) solid var(--ink);
}
.obs-btn--filled-cool {
  background: var(--canvas-cool);
  color: var(--ink);
  border: var(--hairline) solid var(--canvas-cool);
}
.obs-btn--compact {
  padding: var(--space-sm) var(--space-lg);
  min-height: 0;
}
/* "Continue with Google" is set as written in the template, not shouted. */
.obs-btn--sentence { text-transform: none; letter-spacing: 0.6px; }
.obs-btn:hover { opacity: 0.7; }
.obs-btn--filled-cool:hover { opacity: 0.85; }
/* A filled light button on a dark card turns grey if it fades — the template's LOG IN
   stays pure white. Dim the label instead of the plate. */
.obs-btn--filled-light:hover { opacity: 1; color: var(--ink-mute); }
.obs-btn[disabled], .obs-btn[aria-disabled="true"] { opacity: 0.4; cursor: default; }

/* Eyebrow.jsx — all-caps micro label. */
.obs-eyebrow {
  font-family: var(--font-ui);
  font-size: var(--micro-cap-size);
  line-height: var(--micro-cap-lh);
  letter-spacing: var(--micro-cap-ls);
  text-transform: uppercase;
  color: var(--on-primary-mute);
}
.obs-eyebrow--on-light { color: var(--ink-mute); }

/* TextLink.jsx — persistent underline. */
.obs-link { color: var(--link-on-dark); text-decoration: underline; text-underline-offset: 3px; }
.obs-link--on-light { color: var(--link-on-light); }
.obs-link:hover { opacity: 0.7; }

/* Wordmark.jsx — no logo file exists; the mark is set in display type. */
.obs-wordmark {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  font-size: 19px;
  line-height: 1;
  letter-spacing: 1.6px;
  color: var(--on-primary);
  text-decoration: none;
}
.obs-wordmark--on-light { color: var(--ink); }

/* DisplayHeading tones (sizes live in tokens/typography.css). */
.obs-display-xxl, .obs-display-xl, .obs-display-lg { color: var(--on-primary); }
.obs-on-light { color: var(--ink); }
.obs-body-lg, .obs-body-md, .obs-caption { font-family: var(--font-ui); }
.obs-lead { max-width: 560px; color: var(--on-primary-mute); }
.obs-lead--on-light { color: var(--ink-mute); }

/* ---------- navigation ---------- */

/* NavBarOverlay.jsx — transparent over photography, sticky, 24/32 padding.
   The template header is position:relative with no background (WebsiteMain.dc.html 13): a
   static mockup, so "what happens when the page scrolls" was never designed. Our nav is
   sticky, and a transparent sticky bar lets scrolled content read through it — the metrics
   row's rule-top hairline and labels ran straight through the wordmark on index.html.
   Background is therefore --canvas-night always, not just on --solid pages: every page's
   first band already paints --canvas-night under the nav, so scroll-0 renders
   pixel-identical to the transparent version while scrolled content is occluded. */
.obs-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xxl);
  padding: var(--nav-pad-y) var(--nav-pad-x);
  background: var(--canvas-night);
  color: var(--on-primary);
}
/* Light pages keep the dark nav bar rather than inventing a light chrome. Now the same
   value as the base rule; kept because 7 pages + tools/publish_news.py emit the attribute
   that produces it, and deleting it is a larger diff than leaving it inert. */
.obs-nav--solid { background: var(--canvas-night); }
.obs-nav__items { display: flex; align-items: center; gap: var(--space-xxl); }
/* A <button> carrying this class (the sub-menu triggers) otherwise renders with the UA
   `buttonface` grey plate and the UA border; the template's nav is bare text on black
   (WebsiteMain.dc.html 17-23). Reset here rather than on .obs-menu__trigger so any future
   button-as-nav-item inherits the fix. */
.obs-nav__item {
  font-family: var(--font-ui);
  font-size: var(--button-cap-size);
  line-height: var(--button-cap-lh);
  letter-spacing: var(--button-cap-ls);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  color: inherit;
  text-decoration: none;
  opacity: 0.6;
  padding: 0 0 2px;
  border: 0;
  border-bottom: var(--hairline) solid transparent;
  cursor: pointer;
  transition: opacity 160ms linear;
}
.obs-nav__item:hover { opacity: 1; }
.obs-nav__item[aria-current="page"] { opacity: 1; border-bottom-color: currentColor; }
/* Hamburger below 768px, dark overlay kept. No blur, no slide animation. */
.obs-nav__menu { position: relative; }
.obs-nav__toggle { display: none; }
@media (max-width: 767px) {
  .obs-nav__toggle {
    display: inline-block;
    cursor: pointer;
    background: transparent;
    border: 0;
    padding: 0;
    color: inherit;
    font-family: var(--font-ui);
    font-size: var(--button-cap-size);
    letter-spacing: var(--button-cap-ls);
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    min-height: var(--touch-min);
  }
  .obs-nav__toggle[aria-expanded="false"] + .obs-nav__items { display: none; }
  .obs-nav__items {
    position: absolute;
    right: 0;
    top: 100%;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-md);
    padding: var(--space-xl);
    background: var(--canvas-night);
    border: var(--hairline) solid var(--hairline-on-dark);
    min-width: 220px;
  }
}

/* FooterDark.jsx — site-wide dark footer. */
/* Sticky footer: on a short page (the Coming soon slides) the footer would otherwise stop
   partway down and leave empty page background below it. Flex body + `margin-top:auto` on
   the footer pins it to the viewport bottom on short pages and changes nothing on long
   ones. Applied at body level because site.js renders this footer into every page. */
body { display: flex; flex-direction: column; min-height: 100vh; }
.obs-footer { margin-top: auto; }
/* The slack on a short page goes into the last content section, not into a strip of bare
   page background above the footer — otherwise the coming-soon pages show a black band
   between a white slide and the footer. */
body > section:last-of-type { flex: 1 0 auto; }

/* Same band grey as the metrics row (the template footer's translucent grey over black
   resolves to the --canvas-night-band value). */
.obs-footer {
  background: var(--canvas-night-band);
  color: var(--on-primary);
  padding: var(--space-xxl) var(--space-xl);
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  line-height: var(--caption-lh);
}
.obs-footer__cols {
  max-width: var(--reading-column);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-huge);
}
.obs-footer__col { display: flex; flex-direction: column; gap: var(--space-xs); min-width: 160px; }
.obs-footer__col a { color: var(--on-primary-mute); text-decoration: none; font-size: var(--caption-size); }
.obs-footer__col a:hover { opacity: 0.7; }
.obs-footer__legal {
  max-width: var(--reading-column);
  margin: var(--space-xxl) auto 0;
  padding-top: var(--space-md);
  border-top: var(--hairline) solid var(--hairline-on-dark);
  color: var(--on-primary-mute);
  opacity: 0.7;
}

/* ---------- surfaces ---------- */

/* PhotoBand.jsx — full-viewport photographic band, no vertical padding of its own. */
.obs-band {
  position: relative;
  min-height: var(--band-min-height);
  background: var(--canvas-night);
  color: var(--on-primary);
  border-radius: var(--radius-xs);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
}
/* The home hero is sized by its content (template: a 510px inner block sitting directly
   under the nav), not stretched to the viewport — hence the min-height reset. */
.obs-band--center { justify-content: center; min-height: 0; }
/* First band tucks under the nav (matching padding-top keeps its content clear of it), so
   the band's own background, not the page background, meets the nav's bottom edge. */
.obs-band--under-nav { margin-top: -88px; padding-top: 88px; border-radius: 0; }
.obs-band--short { min-height: 72vh; }
.obs-band__inner {
  position: relative;
  width: 100%;
  max-width: var(--reading-column);
  margin: 0 auto;
  padding: var(--space-huge) var(--space-xxl);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xl);
}

/* PhotoPlate.jsx — labelled slot until real graded photography lands. */
.obs-plate {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--canvas-night-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  border: var(--hairline) solid var(--hairline-on-dark);
  font-family: var(--font-ui);
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  text-transform: uppercase;
  color: var(--hairline-on-dark);
}
.obs-plate--sm { border-radius: var(--radius-sm); }
.obs-plate img { width: 100%; height: 100%; object-fit: cover; display: block; filter: var(--photo-grade); }
.obs-plate--strong img { filter: var(--photo-grade-strong); }

/* ContentCard.jsx — light surface, hairline, 8px radius, no shadow, no hover lift. */
.obs-card {
  background: var(--canvas-light);
  border: var(--hairline) solid var(--hairline-on-light);
  border-radius: var(--radius-sm);
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  box-shadow: var(--shadow-none);
}
.obs-card__media {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--canvas-night-soft);
}
.obs-card__eyebrow {
  font-family: var(--font-ui);
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.obs-card__title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--body-md-size);
  line-height: var(--body-md-lh);
  letter-spacing: var(--body-md-ls);
  font-weight: var(--font-weight-bold);
  color: var(--ink);
}
.obs-card__body { margin: 0; font-family: var(--font-ui); font-size: var(--caption-size); line-height: 1.6; color: var(--ink-mute); }
.obs-card__meta { font-family: var(--font-ui); font-size: var(--caption-size); color: var(--ink-mute); }
.obs-card__footer { margin-top: auto; padding-top: var(--space-sm); }

/* VideoFrame.jsx — 16:9 well, outlined play ring, typographic control. */
.obs-video {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--canvas-night);
}
.obs-video__ring {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 72px;
  height: 72px;
  border-radius: var(--radius-full);
  border: var(--hairline) solid var(--on-primary);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--on-primary);
  font-family: var(--font-ui);
  font-size: var(--button-cap-size);
  letter-spacing: var(--button-cap-ls);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 160ms linear;
}
.obs-video__ring:hover { opacity: 0.7; }
.obs-video__duration {
  position: absolute;
  right: var(--space-sm);
  bottom: var(--space-sm);
  font-family: var(--font-ui);
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  text-transform: uppercase;
  color: var(--on-primary);
}

/* ---------- data (visual only — no sweep data source is wired yet) ---------- */

/* DataTable.jsx — hairline-ruled, caps headers, no fills. */
.obs-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  color: var(--ink);
}
.obs-table th {
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  text-transform: uppercase;
  color: var(--ink-mute);
  text-align: left;
  font-weight: var(--font-weight-regular);
  padding: var(--space-xs) var(--space-sm);
  border-bottom: var(--hairline) solid var(--hairline-on-light);
}
.obs-table td {
  padding: var(--space-sm);
  border-bottom: var(--hairline) solid var(--hairline-on-light);
  white-space: nowrap;
}

/* MetricReadout.jsx — display-tier figure over a caps label. */
.obs-metric { display: flex; flex-direction: column; gap: var(--space-xxs); }
.obs-metric__label {
  font-family: var(--font-ui);
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  line-height: 1.6;
  text-transform: uppercase;
  color: var(--ink-mute);
}
/* Template (WebsiteMain.dc.html 69-72): 40px, and NOT uppercased — "Free (Open Beta)"
   is set as written. At 48px+uppercase the fee readout alone was wide enough to wrap the
   five-metric row onto two lines. */
.obs-metric__value {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: 40px;
  line-height: 1.25;
  letter-spacing: var(--display-lg-ls);
  color: var(--ink);
  white-space: nowrap;
}
.obs-metric__unit { font-size: 0.4em; letter-spacing: var(--micro-cap-ls); margin-left: 0.35em; }
.obs-metric--on-dark .obs-metric__label { color: var(--on-primary-mute); }
.obs-metric--on-dark .obs-metric__value { color: var(--on-primary); }
/* A pill inside a metric label (index "Order" link) must not push its value below the
   row's other values — pull the pill's extra height out of the line box. */
.obs-metric__label .obs-btn { margin-block: -3px; }

/* ---------- layout ---------- */

.obs-section { background: var(--canvas-light); padding: var(--space-section-app) var(--space-xxl); }
.obs-section--dark-soft { background: var(--canvas-night-band); color: var(--on-primary); padding: var(--space-huge) var(--space-xxl); }
.obs-section--dark { background: var(--canvas-night); color: var(--on-primary); padding: var(--space-section-app) var(--space-xxl); }
.obs-column { width: 100%; max-width: var(--reading-column); margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-xxl); }
.obs-rule-top { border-top: var(--hairline) solid var(--hairline-on-dark); padding-top: var(--space-huge); }
/* width:100% keeps `margin:0 auto` from cancelling flex stretch inside .obs-column. */
.obs-metric-row { width: 100%; max-width: var(--reading-column); margin: 0 auto; display: flex; gap: var(--space-huge); flex-wrap: wrap; }
/* Card grid stair-steps 4-up -> 2-up -> 1-up. */
.obs-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-xxl); }
.obs-grid--split { grid-template-columns: 1.4fr 1fr; align-items: start; }
@media (max-width: 960px) { .obs-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 767px) { .obs-grid, .obs-grid--split { grid-template-columns: 1fr; } }

/* Scale ladder — four model viewports divided by hairlines (website-main template).
   The 3D geometry asset (assets/3d/battery-models.js) is owned by software-dev; each
   cell renders a labelled slot with the same hook name until that asset is wired in. */
.obs-ladder { flex: 1; min-height: 56vh; display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: var(--hairline) solid var(--hairline-on-dark); }
.obs-ladder__cell { position: relative; display: flex; flex-direction: column; padding: var(--space-xs) 0 var(--space-xl); }
.obs-ladder__cell + .obs-ladder__cell { border-left: var(--hairline) solid var(--hairline-on-dark); }
.obs-ladder__slot { position: relative; flex: 1; min-height: 0; }
.obs-ladder__label { position: relative; padding: 0 var(--space-xl); font-size: var(--micro-cap-size); line-height: 1.6; letter-spacing: var(--micro-cap-ls); text-transform: uppercase; color: var(--on-primary); }
@media (max-width: 767px) {
  .obs-ladder { grid-template-columns: repeat(2, 1fr); }
  .obs-ladder__cell:nth-child(odd) { border-left: 0; }
}

/* Profile card (website-main "OpenBatterySim administrator" band). */
.obs-profile { display: flex; flex-direction: column; gap: var(--space-sm); }
.obs-profile__photo {
  width: 104px;
  height: 104px;
  border-radius: var(--radius-full);
  border: var(--hairline) solid var(--hairline-on-dark);
  background: var(--canvas-night-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--micro-cap-size);
  letter-spacing: var(--micro-cap-ls);
  text-transform: uppercase;
  color: var(--hairline-on-dark);
}
/* Name and role are one tight pair (template gap 2px), not two stacked blocks. */
.obs-profile__identity { display: flex; flex-direction: column; gap: 2px; }
.obs-profile__name { font-size: var(--body-md-size); font-weight: var(--font-weight-bold); letter-spacing: var(--body-md-ls); color: var(--on-primary); }
.obs-profile__role { font-size: var(--caption-size); line-height: 1.5; color: var(--on-primary-mute); opacity: 0.65; }
.obs-profile__bio { margin: 0; font-size: var(--caption-size); line-height: 1.6; color: var(--on-primary-mute); opacity: 0.8; }
.obs-profile__tags { font-size: var(--micro-cap-size); line-height: 1.7; letter-spacing: var(--micro-cap-ls); text-transform: uppercase; color: var(--on-primary-mute); opacity: 0.5; }
/* Square outlined icon buttons (template 87-90), not underlined word links. */
.obs-profile__links { display: flex; gap: var(--space-sm); }
.obs-profile__links a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-sm);
  color: var(--on-primary);
  text-decoration: none;
}
.obs-profile__links a:hover { border-color: var(--on-primary); }
/* These are placeholders for real profile URLs, not dated roadmap entries — the template
   draws them at full strength, so opt them out of the global dimming while keeping them
   unclickable. */
.obs-profile__links a[aria-disabled="true"] { opacity: 1; }
/* Template's CONTACT pill: 6/14 padding at 11px, smaller than --compact. */
.obs-btn--micro { padding: 6px 14px; min-height: 0; font-size: 11px; line-height: 1; }

/* Form fields — native inputs at token scale (inputs pad 12/16, radius 4px).
   components/forms is out of scope for this task, so no TextInput/SelectField port. */
.obs-field { display: flex; flex-direction: column; gap: var(--space-xxs); }
.obs-field__label { font-family: var(--font-ui); font-size: var(--micro-cap-size); letter-spacing: var(--micro-cap-ls); text-transform: uppercase; color: var(--ink-mute); }
.obs-field input, .obs-field textarea {
  font-family: var(--font-ui);
  font-size: var(--body-md-size);
  line-height: var(--body-md-lh);
  color: var(--ink);
  background: var(--canvas-light);
  border: var(--hairline) solid var(--hairline-on-light);
  border-radius: var(--radius-xs);
  padding: var(--input-pad-y) var(--input-pad-x);
  min-height: var(--touch-min);
}
.obs-field textarea { resize: vertical; }
.obs-status { font-family: var(--font-ui); font-size: var(--caption-size); color: var(--ink-mute); }

.obs-stack { display: flex; flex-direction: column; gap: var(--space-sm); }
/* nowrap per WebsiteMain.dc.html line 36 — the three download buttons stay on one line
   at any width and shrink instead of wrapping. Only the home hero uses .obs-row.
   Template button metrics (12/16 pad, 11.5px) so the row fits the 544px hero column;
   gap is the template's 8px. */
.obs-row { display: flex; flex-wrap: nowrap; gap: var(--space-xs); }
.obs-row .obs-btn { padding: var(--space-sm) var(--space-md); min-height: 0; font-size: 11.5px; line-height: .94; letter-spacing: 1px; }

/* The component classes below set `display`, which outranks the UA rule for [hidden].
   Without this, hiding a pane with the attribute silently does nothing. */
[hidden] { display: none !important; }

/* ---------- nav dropdown (website-main: SIMULATION / LECTURE / HUB submenus) ----------
   Opens on hover and stays open until a click lands outside the menu (obsBindExpanders);
   a CSS :hover rule cannot express "stays open", so visibility is driven by aria-expanded
   alone and JS owns both the open and the close. Panel is the night-soft surface with a
   hairline, no blur. */
.obs-menu { position: relative; display: flex; align-items: center; }
.obs-menu__panel {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: var(--space-xs);
  min-width: 180px;
  background: var(--canvas-night-panel);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-sm);
  padding: var(--space-xs);
  display: none;
  flex-direction: column;
  gap: 2px;
  z-index: 40;
}
.obs-menu__trigger[aria-expanded="true"] + .obs-menu__panel { display: flex; }
/* The panel sits `margin-top` below the trigger; this pad bridges that gap so the pointer
   can travel trigger -> panel without leaving the menu's hover area. */
.obs-menu__panel::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: var(--space-xs); }
.obs-menu__panel a {
  padding: var(--space-xs) var(--space-sm);
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  letter-spacing: var(--body-md-ls);
  text-transform: none;
  color: var(--on-primary-mute);
  text-decoration: none;
  border-radius: var(--radius-xs);
}
.obs-menu__panel a:hover { color: var(--on-primary); opacity: 1; }
/* Roadmap entries and not-yet-wired controls: shown, dated, not reachable. One rule for
   every surface so a dated item never navigates to "#" and dirties the URL. */
a[aria-disabled="true"] { opacity: 0.32; pointer-events: none; }
.obs-btn[aria-disabled="true"] { opacity: 0.4; pointer-events: none; }

/* Signed-in identity chip in the nav. The nickname is a .obs-menu__trigger: clicking or
   hovering it opens Plan / Profile / Log out. */
.obs-nav__user { display: flex; align-items: center; gap: var(--space-xs); }
.obs-nav__avatar {
  width: 24px; height: 24px;
  border-radius: var(--radius-full);
  background: var(--hairline-on-dark);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-ui); font-size: 11px; color: var(--on-primary);
  overflow: hidden;
}
.obs-nav__avatar img { width: 100%; height: 100%; object-fit: cover; }
.obs-nav__nick { font-family: var(--font-ui); font-size: var(--caption-size); color: var(--on-primary); opacity: .85; text-transform: none; letter-spacing: 0; }
.obs-nav__nick:hover { opacity: 1; }
/* Account panel hangs off the right edge — it is the last item in the row. */
.obs-menu__panel--right { left: auto; right: 0; }
/* Non-interactive row inside a menu panel (the plan readout): same metrics as the links,
   no pointer affordance. */
.obs-menu__static {
  padding: var(--space-xs) var(--space-sm);
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  letter-spacing: var(--body-md-ls);
  color: var(--on-primary-mute);
  opacity: .55;
  cursor: default;
}

/* ---------- modal (login / download) ----------
   Scrim + centred card. No blur, no motion beyond the 160ms opacity the buttons use.
   Template scrim is rgba(0,0,0,.6) over a --canvas-night-panel card, i.e. the card reads
   as a raised grey plate rather than a near-black hole. Both modals share these classes,
   so the download modal picks up the same surface. */
.obs-modal {
  position: fixed;
  inset: 0;
  background: var(--canvas-night);
  opacity: 0.6;
  display: none;
  z-index: 100;
}
.obs-modal[open], .obs-modal--open { display: block; }
.obs-modal__wrap {
  position: fixed;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl);
  z-index: 101;
}
.obs-modal__wrap--open { display: flex; }
.obs-modal__card {
  background: var(--canvas-night-panel);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-sm);
  padding: var(--space-xxl);
  width: 380px;
  max-width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  color: var(--on-primary);
}
.obs-modal__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--micro-cap-ls);
  font-size: 14px;
  line-height: 1.4;
}
.obs-modal__card input {
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  color: var(--on-primary);
  background: var(--canvas-night);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-xs);
  padding: var(--space-sm);
  min-height: var(--touch-min);
}
.obs-modal__error { font-family: var(--font-ui); font-size: var(--micro-cap-size); color: var(--on-primary); }
.obs-modal__error[hidden] { display: none; }
.obs-btn--filled-light { background: var(--canvas-light); color: var(--ink); border: var(--hairline) solid var(--canvas-light); }
.obs-rule-or { display: flex; align-items: center; gap: var(--space-xs); }
.obs-rule-or::before, .obs-rule-or::after { content: ""; flex: 1; height: var(--hairline); background: var(--hairline-on-dark); }
.obs-rule-or span { font-family: var(--font-ui); font-size: var(--micro-cap-size); letter-spacing: var(--micro-cap-ls); color: var(--on-primary-mute); opacity: .5; }
.obs-modal__spec { border-top: var(--hairline) solid var(--hairline-on-dark); padding-top: var(--space-md); display: flex; flex-direction: column; gap: var(--space-xxs); }
.obs-spec-grid { display: grid; grid-template-columns: auto 1fr; gap: var(--space-xxs) var(--space-sm); font-family: var(--font-ui); font-size: var(--micro-cap-size); line-height: 1.6; color: var(--on-primary-mute); }
.obs-spec-grid dt { opacity: .6; margin: 0; }
.obs-spec-grid dd { margin: 0; }
.obs-list__item {
  padding: var(--space-xs) var(--space-sm);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-xs);
  font-family: var(--font-ui);
  font-size: var(--caption-size);
  color: var(--on-primary-mute);
  text-decoration: none;
}
.obs-modal__links { display: flex; justify-content: space-between; gap: var(--space-md); font-family: var(--font-ui); font-size: var(--micro-cap-size); letter-spacing: .4px; }
/* Underlined and readable: the template draws these at full strength even though the
   endpoints behind them do not exist yet (they are not dated roadmap entries). */
.obs-modal__links a, .obs-modal__links a[aria-disabled="true"] { color: var(--on-primary-mute); opacity: 1; }

/* ---------- home hero: headline column + 2x2 scale grid (website-main second band) ----
   The template's four scale cells are a hairline-divided 2x2 block; each cell opens an
   overlay listing the sub-models available at that scale. */
/* Template geometry (WebsiteMain.dc.html 32): 1200px column, 56px/32px padding, 48px gap.
   The 2x2 grid therefore fills a ~544px half-column and the band's height follows from it
   instead of being stretched to 100vh — which is what kept the scale images small and
   pushed the hero into the middle of the viewport. */
/* minmax(0,...) rather than a bare 1fr: the headline's min-content width otherwise wins
   the track sizing and steals ~190px from the scale grid, which is what made the four
   models render small. */
/* Template geometry (WebsiteMain.dc.html 32): 1200px column, 56/32 padding, 48px gap and
   a 510px-tall inner block. The fixed height is load-bearing — the square scale cells are
   sized by it, so the grid ends up ~490px wide and the whole band stays just under
   600px, which is what puts the hero right under the nav instead of mid-viewport. */
.obs-hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-huge); align-items: center; width: 100%; max-width: var(--reading-column); margin: 0 auto; padding: 56px var(--space-xxl); height: 622px; /* 510px content + 2x56 pad; the template is content-box, this stylesheet is border-box */ }
/* Template headline metrics (WebsiteMain.dc.html 36): 48px / .98 / 1.7px. */
.obs-hero h1 { line-height: .98; letter-spacing: 1.7px; }
/* Grid is height-bound, so it sits at the right edge of its track like the template. */
.obs-hero .obs-scale-grid { height: 100%; width: auto; aspect-ratio: 1; justify-self: end; }
@media (max-width: 960px) { .obs-hero { height: auto; } .obs-hero .obs-scale-grid { height: auto; width: 100%; justify-self: stretch; } }
@media (max-width: 960px) { .obs-hero { grid-template-columns: 1fr; } }
.obs-scale-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--hairline);
  background: var(--hairline-on-dark);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.obs-scale {
  position: relative;
  background: var(--canvas-night);
  padding: var(--space-md);
  aspect-ratio: 1;
  cursor: pointer;
  border: 0;
  text-align: left;
  display: flex;
  flex-direction: column;
  color: var(--on-primary);
}
.obs-scale__stage { position: relative; width: 100%; height: 75%; }
/* <battery-model> from assets/3d/battery-models.js renders its canvas here; the
   sub-model overlay (.obs-scale__subs, z-index 40) sits above it. */
.obs-scale__slot { position: absolute; inset: 0; z-index: 1; }
.obs-scale__label { padding-top: var(--space-xs); font-family: var(--font-ui); font-size: var(--micro-cap-size); line-height: 1.6; letter-spacing: var(--micro-cap-ls); text-transform: uppercase; color: var(--on-primary); }
/* Template (WebsiteMain.dc.html 43/49/55/61): a light scrim over the still-visible 3D
   model, and a translucent blurred card on top — the geometry reads through both. The
   card scrolls when the sub-model list is longer than the cell. */
.obs-scale__subs {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgb(0 0 0 / 30%);
  z-index: 40;
}
.obs-scale[aria-expanded="true"] .obs-scale__subs { display: flex; }
.obs-scale__subs > div {
  background: rgb(20 20 20 / 40%);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  border: var(--hairline) solid rgb(255 255 255 / 25%);
  border-radius: var(--radius-sm);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 70%;
  max-width: 88%;
  max-height: 92%;
  overflow-y: auto;
  /* A scrollbar has to be permanently visible for "there is more below" to read; the
     platform default here is an overlay bar that only surfaces mid-scroll and takes no
     layout space. `scrollbar-width/-color` is the standard property that turns that off
     (Chrome 121+, Firefox); the -webkit- pseudo-elements below cover Safari. */
  scrollbar-width: thin;
  scrollbar-color: rgb(255 255 255 / 45%) rgb(255 255 255 / 12%);
}
.obs-scale__subs > div::-webkit-scrollbar { width: 8px; }
.obs-scale__subs > div::-webkit-scrollbar-track { background: rgb(255 255 255 / 12%); border-radius: var(--radius-xs); }
.obs-scale__subs > div::-webkit-scrollbar-thumb { background: rgb(255 255 255 / 45%); border-radius: var(--radius-xs); }
.obs-scale__subs a {
  padding: var(--space-xs) 10px;
  font-family: var(--font-ui);
  font-size: 12px;
  letter-spacing: .4px;
  color: var(--on-primary);
  text-decoration: none;
  border-radius: var(--radius-xs);
}

/* ---------- news list (website-news) ---------- */
.obs-news-top { display: grid; grid-template-columns: 2fr 1fr; gap: var(--space-xl); }
@media (max-width: 960px) { .obs-news-top { grid-template-columns: 1fr; } }
.obs-feature { position: relative; border-radius: var(--radius-sm); overflow: hidden; aspect-ratio: 16 / 10; display: block; text-decoration: none; color: var(--on-primary); }
/* Template (WebsiteNews.dc.html 21): the caption sits on a bottom-up gradient so the
   image fades into the text block instead of ending at a hard edge. A flat panel at 88%
   opacity, which is what this was, reads as a second box stacked on the photo. */
.obs-feature__caption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-xl);
  background: linear-gradient(to top, rgb(0 0 0 / 88%), rgb(0 0 0 / 10%) 80%);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  z-index: 2;
}
.obs-feature__title { margin: 0; font-family: var(--font-display); font-weight: var(--font-weight-bold); font-size: 26px; line-height: 1.1; color: var(--on-primary); }
.obs-meta { font-family: var(--font-ui); font-size: 11px; letter-spacing: .6px; text-transform: uppercase; color: var(--on-primary-mute); opacity: .6; }
.obs-byline { font-family: var(--font-ui); font-size: var(--caption-size); color: var(--on-primary-mute); opacity: .7; }
.obs-side { display: flex; flex-direction: column; gap: var(--space-md); }
.obs-side__item { display: flex; gap: 14px; text-decoration: none; color: var(--on-primary); flex: 1; min-height: 0; }
.obs-side__thumb { width: 170px; aspect-ratio: 4 / 3; flex: none; border-radius: var(--radius-sm); overflow: hidden; position: relative; background: var(--canvas-night-soft); }
.obs-news-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-xl); }
@media (max-width: 960px) { .obs-news-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 599px) { .obs-news-grid { grid-template-columns: 1fr; } }
.obs-news-item { display: flex; flex-direction: column; gap: 10px; text-decoration: none; color: var(--on-primary); }
.obs-news-item__thumb { width: 100%; aspect-ratio: 4 / 3; border-radius: var(--radius-sm); overflow: hidden; position: relative; background: var(--canvas-night-soft); }
.obs-news-item__title, .obs-side__title { font-family: var(--font-ui); font-size: 13px; font-weight: var(--font-weight-bold); line-height: 1.3; text-wrap: pretty; color: var(--on-primary); }
.obs-news-item__date, .obs-side__date { font-family: var(--font-ui); font-size: 11px; color: var(--on-primary-mute); opacity: .55; }

/* ---------- article page (website-news-page) ---------- */
.obs-article { width: 100%; max-width: 760px; margin: 0 auto; padding: var(--space-xl) var(--space-xxl) 80px; display: flex; flex-direction: column; gap: var(--space-xl); }
.obs-article__back { font-family: var(--font-ui); font-size: var(--micro-cap-size); letter-spacing: .6px; color: var(--on-primary-mute); text-decoration: none; opacity: .6; }
.obs-article__head { display: flex; flex-direction: column; gap: var(--space-sm); }
.obs-article__title { margin: 0; font-family: var(--font-display); font-weight: var(--font-weight-bold); font-size: 34px; line-height: 1.1; color: var(--on-primary); }
.obs-article__byline { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.obs-article__hero { width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius-sm); overflow: hidden; position: relative; background: var(--canvas-night-soft); }
.obs-article__body { display: flex; flex-direction: column; gap: var(--space-md); font-family: var(--font-ui); font-size: 15px; line-height: 1.7; color: var(--on-primary-mute); opacity: .9; }
.obs-article__body p { margin: 0; }
.obs-share { position: relative; }
.obs-share__panel {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: var(--space-xs);
  min-width: 180px;
  background: var(--canvas-night-panel);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--radius-sm);
  padding: var(--space-xs);
  display: none;
  flex-direction: column;
  gap: 2px;
  z-index: 10;
}
.obs-share__trigger[aria-expanded="true"] + .obs-share__panel { display: flex; }
.obs-share__panel a { padding: var(--space-xs) var(--space-sm); font-family: var(--font-ui); font-size: var(--caption-size); color: var(--on-primary-mute); text-decoration: none; border-radius: var(--radius-xs); }
.obs-share__panel a:hover { color: var(--on-primary); opacity: 1; }

/* Dark single-column form (.om*): order-model.html, profile.html, admin-profile.html.
   Moved verbatim from order-model.html so the three pages share one definition. */
.om{max-width:720px;margin:0 auto;padding:var(--space-xxl) var(--space-xxl) 96px;display:flex;flex-direction:column;gap:22px;font-family:var(--font-ui)}
.om-head{display:flex;flex-direction:column;gap:10px}
.om-cap{font-size:11px;letter-spacing:.8px;text-transform:uppercase;opacity:.6}
.om-title{margin:0;font-family:var(--font-display);font-weight:700;font-size:38px;line-height:1.05;text-transform:uppercase;letter-spacing:.6px;color:var(--on-primary)}
.om-lead{margin:0;font-size:14px;line-height:1.6;opacity:.7}
.om-field{display:flex;flex-direction:column;gap:var(--space-xs);position:relative}
.om input,.om textarea,.om-date{border:var(--hairline) solid var(--hairline-on-dark);border-radius:var(--radius-xs);padding:var(--space-sm);font-size:14px;font-family:inherit;background:var(--canvas-night-soft);color:var(--on-primary-mute);outline:none}
.om textarea{resize:vertical}
.om input::placeholder,.om textarea::placeholder{color:var(--on-primary-mute);opacity:.35}
.om input:focus,.om textarea:focus,.om-date:focus-visible{border-color:var(--on-primary-mute)}
.om-row{display:flex;align-items:center;gap:var(--space-lg);flex-wrap:wrap}
.om-submit{align-self:flex-start;margin-top:var(--space-xs);padding:13px 28px;border:var(--hairline) solid var(--on-primary);border-radius:var(--radius-pill);background:var(--on-primary);color:var(--ink);font-family:inherit;font-size:12.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase;cursor:pointer}
.om-status{margin:0;font-size:12.5px;opacity:.7}
