/* ==========================================================================
   game-console-ui-theme / shell
   --------------------------------------------------------------------------
   The dashboard: ground, tile rows, the focus ring, full-screen views, the
   hint bar. This file is the shell class's equivalent of a register theme's
   play layer, except it is not a garnish; it is the archetype. Delete it
   and shell.js and you still have a readable page built from tokens.css and
   components.css, which is the JS-last contract this class is built on.

   HOW THE PROGRESSIVE ENHANCEMENT WORKS. The markup is an ordinary
   document: a header, sections of anchor links, article sections in reading
   order, a footer. This file styles that document twice:

     1. The base styles below assume no JavaScript. Rows wrap instead of
        scrolling sideways, every tile is a visible link, every view is a
        titled section further down the page, and the page scrolls like a
        page. Everything is reachable and readable.

     2. shell.js puts `tv-live` on the <html> element as its first act.
        Every rule scoped under .tv-live upgrades the same markup into a
        dashboard: the page stops scrolling, rows run off the right edge
        and scroll, views become full-screen surfaces that cover the browse
        surface, and the hint bar rewrites itself to name the controls that
        now exist. No markup is duplicated; the document is the dashboard.

   THE ONE THING THIS SHELL IS ABOUT. Everything here is arranged so the
   focus ring wins. Tiles rest quietly (a 1.16:1 fill step off the ground,
   a 1.68:1 edge); the ring lands at 10.39:1 against the same ground. That
   ordering is the design, not a detail of it, because a surface driven
   without a pointer has to answer "where am I" from across a room.

   There is not one colour literal in this file, and every length that
   carries a design decision is a token from tokens.css. Audited: the file
   contains exactly one px literal, `left: -9999px`, which is where the skip
   link parks itself offscreen and is not a design value. The rest of what is
   not a token is not a decision either: viewport units, the 767px
   breakpoint, and the percentages inside one decorative gradient.

   Requires tokens.css. Load it, then components.css, then this, then the
   script:

     <link rel="stylesheet" href="tokens/tokens.css" />
     <link rel="stylesheet" href="components/components.css" />
     <link rel="stylesheet" href="shell/shell.css" />
     <script src="shell/shell.js" defer></script>
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE FIELD, AND THE GROUND SYSTEM
   The body is the field the dashboard sits on, and that ground is a slot,
   not a colour. Two axes stay independent: the register (tokens.css) says
   what colours exist, the ground (a data attribute on body) says how the
   field composes them. This is the mechanism the shell class's pilot
   established as a wallpaper system; the axis is renamed here because what
   varies on a room-scale screen is the light in the room, not a picture
   hung behind the furniture.

   THE MECHANISM (class standard for shell themes):
     body[data-ground="flat"]   the plain field, also what you get with no
                                attribute at all, so the no-JS document
                                carries it by plain CSS
     body[data-ground="panes"]  reveals the inline SVG scene layer

   shell.js only toggles the attribute; every visual belongs to these
   attribute-scoped rules. The picker, like all behaviour, exists only after
   enhancement.

   CONTRAST BY CONSTRUCTION, INVERTED. The pilot restricted its wallpaper to
   the light end of the palette so dark ink stayed legible over it. This
   register is dark, so the restriction inverts: the scene layer may paint
   ONLY with ground, scene, chrome, surface, surface-raised and border. The
   LIGHTEST of those is surface-raised, and full ink over it measures
   11.79:1, so the worst text-over-scene pairing on the field (a row
   heading, full ink) clears AA more than twice over without a scrim.
   Measured over every allowed paint: chrome 17.96, ground 17.31, scene
   15.62, surface 14.94, surface-raised 11.79, border 10.31. Tiles, views
   and the hint bar are opaque surfaces with their own measured pairings, so
   the scene cannot reach the text inside them. The class guarantee, should
   a future variant want brighter paint: a token-driven scrim layer between
   scene and content, with the ratios re-measured through the scrim. No
   shipped variant needs one.

   The flat field: a soft pool of light spilling from above the first row,
   drawn with the scene tone in a single radial gradient. Decorative;
   nothing depends on seeing it.
   -------------------------------------------------------------------------- */
.tv-field {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background-color: var(--tv-ground);
  background-image: radial-gradient(
    130% 70% at 50% -8%,
    var(--tv-scene),
    transparent 62%
  );
  background-repeat: no-repeat;
  color: var(--tv-ink);
  font-family: var(--tv-font-sans);
  font-size: var(--tv-text-body);
  font-weight: var(--tv-weight-body);
  line-height: var(--tv-leading-body);
}

/* The scene layer: an inline SVG in the page markup, because an SVG loaded
   through an image or a data URI is an isolated document that cannot read
   custom properties. Inlined, the paint rules below reach it, and a re-skin
   of tokens.css re-skins the scene with everything else. Hidden until the
   body attribute asks for it, so it costs the no-JS document nothing but
   bytes. */
.tv-wallpaper {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.tv-wallpaper > svg {
  width: 100%;
  height: 100%;
}

body[data-ground="panes"] .tv-wallpaper {
  display: block;
}

/* Scene paint rules. Only the allowed tokens from the contrast contract
   above. */
.tv-wallpaper .tv-art-line {
  stroke: var(--tv-border);
  fill: none;
}

.tv-wallpaper .tv-art-pane {
  fill: var(--tv-scene);
  stroke: var(--tv-border);
}

.tv-wallpaper .tv-art-lit {
  fill: var(--tv-surface);
  stroke: none;
}

.tv-wallpaper .tv-art-edge {
  fill: var(--tv-surface-raised);
  stroke: none;
}

/* Anyone who has asked for more contrast gets the flat field regardless of
   what the picker says. Placed after the scene rule so it wins on source
   order at equal specificity. */
@media (prefers-contrast: more) {
  body[data-ground] .tv-wallpaper {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   THE LIVE LAYOUT
   Live, the page stops being a page: the field becomes a three-band grid,
   the middle band scrolls and the two chrome bands do not. Without the
   script none of this applies and the document scrolls normally.
   -------------------------------------------------------------------------- */
.tv-live,
.tv-live body {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

/* minmax(0, 1fr) on the column, not a bare 1fr and not nothing at all. An
   implicit auto column sizes to the widest thing inside it, and the widest
   thing inside this one is a row of tiles that is meant to run off the edge,
   so the whole dashboard would grow to the width of its longest row and the
   row would never scroll. The 0 minimum is what lets the track shrink below
   its content and hand the overflow back to the row where it belongs. */
.tv-live .tv-field {
  display: grid;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: minmax(0, 1fr);
}

.tv-live .tv-main {
  min-height: 0; /* lets the scrolling band actually shrink */
}

/* --------------------------------------------------------------------------
   SKIP LINK
   Above everything, including an open view.
   -------------------------------------------------------------------------- */
.tv-skip {
  position: absolute;
  left: -9999px;
}

.tv-skip:focus {
  left: var(--tv-space-4);
  top: var(--tv-space-4);
  z-index: var(--tv-z-skip);
  padding: var(--tv-space-3) var(--tv-space-4);
  color: var(--tv-ink);
  background: var(--tv-surface);
  border: var(--tv-border-width) solid var(--tv-border-strong);
  border-radius: var(--tv-radius);
  font-weight: var(--tv-weight-bold);
}

/* --------------------------------------------------------------------------
   BRAND
   The document header. As a page it is a normal masthead; live it stays a
   masthead, because a screen read from ten feet has room for one and
   because shrinking it to a chip would make it unreadable at the distance
   this register is designed for. Only the tagline goes, which is a sentence
   about the page rather than a part of the dashboard.
   -------------------------------------------------------------------------- */
/* The brand block and the board are lifted into the positioned layer, above
   the scene. A fixed element with z-index 0 paints above ordinary
   block-level content no matter where it sits in the document, so without
   these two lines the scene draws over every row heading: tiles survive it
   only because they are positioned themselves. Found by looking at a render,
   which is the only way this class of bug is ever found. */
.tv-brand,
.tv-rows {
  position: relative;
  z-index: var(--tv-z-rows);
}

.tv-brand {
  padding: var(--tv-space-6) var(--tv-rail) var(--tv-space-3);
}

.tv-brand-name {
  margin: 0;
  font-size: var(--tv-text-h1);
  font-weight: var(--tv-weight-display);
  letter-spacing: var(--tv-tracking-tight);
  line-height: var(--tv-leading-tight);
}

.tv-brand-tagline {
  margin: var(--tv-space-3) 0 0;
  max-width: var(--tv-measure);
  color: var(--tv-ink-muted);
}

.tv-live .tv-brand-tagline {
  display: none;
}

/* --------------------------------------------------------------------------
   ROWS
   The navigation surface. As a page: sections whose tile lists wrap, so
   every tile is visible and nothing is hidden off an edge that cannot
   scroll without a script. Live: each row is a single line that runs off
   the right edge and scrolls, which is the shape this archetype is.
   -------------------------------------------------------------------------- */
.tv-rows {
  padding-bottom: var(--tv-space-4);
}

.tv-live .tv-rows {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* While a view is open the browse surface is completely covered. It leaves
   the tab order and the accessibility tree with it, so Tab inside a view
   never lands on something the reader cannot see. visibility rather than
   display, because visibility keeps the layout and therefore keeps every
   row's horizontal scroll position for when the reader comes back. */
.tv-live .tv-rows[data-tv-hidden] {
  visibility: hidden;
}

.tv-row {
  margin: 0 0 var(--tv-row-gap);
}

.tv-row-title {
  margin: 0;
  padding: 0 var(--tv-rail);
  font-size: var(--tv-text-h2);
  font-weight: var(--tv-weight-display);
  line-height: var(--tv-leading-tight);
  letter-spacing: var(--tv-tracking-tight);
}

/* The track. Its padding is not decoration: a focused tile grows and wears
   a ring held off its edge, and an overflow container clips both. The
   padding is the room the ring needs, and the negative side margin pulls
   the tiles back onto the rail so the row still lines up with its heading.

   The scrollbar is hidden deliberately. Every tile in a row is in the tab
   order and reachable with the arrow keys, so nothing here is reachable
   only by dragging; what tells a reader the row continues is the next tile
   showing at the edge, which is the affordance this shape has always used.
   -------------------------------------------------------------------------- */
.tv-row-track {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tv-tile-gap);
  margin: 0;
  padding: var(--tv-space-3) var(--tv-rail);
  list-style: none;
}

.tv-live .tv-row-track {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}

.tv-live .tv-row-track::-webkit-scrollbar {
  display: none;
}

/* --------------------------------------------------------------------------
   TILE
   The unit of the whole archetype. A link, sized near 16:9 because what a
   tile stands for on a screen like this usually is. Its resting state is
   quiet on purpose; see the note at the top of this file.
   -------------------------------------------------------------------------- */
.tv-tile {
  position: relative; /* so a focused tile can lift above its neighbours */
  flex: none;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--tv-space-1);
  width: var(--tv-tile-w);
  height: var(--tv-tile-h);
  padding: var(--tv-space-4);

  color: var(--tv-ink);
  text-decoration: none;
  background: var(--tv-surface);
  border: var(--tv-border-width) solid var(--tv-border);
  border-radius: var(--tv-radius-tile);
  box-shadow: var(--tv-shadow-tile);

  transition:
    transform var(--tv-duration) var(--tv-ease),
    background-color var(--tv-duration) var(--tv-ease),
    box-shadow var(--tv-duration) var(--tv-ease);
}

/* The mark. An original line glyph, painted with currentColor so it reads
   the tokens like everything else, pushed to the top of the tile by the
   auto margin so labels line up across a row whatever the glyph is. */
.tv-tile-glyph {
  display: block;
  width: var(--tv-glyph-size);
  height: var(--tv-glyph-size);
  margin-bottom: auto;
  color: var(--tv-ink-muted);
}

.tv-tile-glyph > svg {
  width: 100%;
  height: 100%;
}

.tv-tile-label {
  font-size: var(--tv-text-h3);
  font-weight: var(--tv-weight-bold);
  line-height: var(--tv-leading-snug);
  letter-spacing: var(--tv-tracking-tight);
}

.tv-tile-note {
  font-size: var(--tv-text-xs);
  color: var(--tv-ink-muted);
  line-height: var(--tv-leading-snug);
}

/* A chip in the tile's top corner, for the one word a tile sometimes has to
   carry that is not its name. It borrows the badge component rather than
   inventing a second chip, which is also what makes it show up honestly in
   the re-skin failure CUSTOMIZE.md documents. */
.tv-tile-flag {
  position: absolute;
  top: var(--tv-space-3);
  right: var(--tv-space-3);
}

.tv-tile:hover {
  background: var(--tv-surface-raised);
}

.tv-tile:hover .tv-tile-glyph {
  color: var(--tv-ink);
}

/* THE HERO ELEMENT. Five pixels of accent, held four off the tile, over a
   tile that has lifted and grown. Nothing else on the browse surface is
   allowed to look like this. */
.tv-tile:focus-visible {
  z-index: 1;
  outline: var(--tv-ring-width) solid var(--tv-ring);
  outline-offset: var(--tv-ring-offset);
  background: var(--tv-surface-raised);
  box-shadow: var(--tv-shadow-raised);
  transform: scale(var(--tv-focus-scale));
}

.tv-tile:focus-visible .tv-tile-glyph {
  color: var(--tv-accent);
}

/* --------------------------------------------------------------------------
   VIEW
   What a tile opens. As a page it is a titled section in reading order,
   which is the entire no-JS experience of this shell's content. Live it is
   a full-screen surface that covers the browse surface and stops short of
   the hint bar, so the control legend is true at every moment including
   this one.

   The view is its own scroll container AND the element that takes focus on
   open, which means a keyboard reader can scroll it with the arrow keys the
   moment it opens without hunting for a scroll region. shell.js adds the
   dialog semantics at enhance time; the no-JS document claims none of it.
   -------------------------------------------------------------------------- */
.tv-view {
  max-width: var(--tv-view-width);
  margin: 0 auto var(--tv-space-12);
  padding: 0 var(--tv-rail);
}

.tv-view-head {
  display: flex;
  align-items: center;
  gap: var(--tv-space-4);
  margin: 0 0 var(--tv-space-6);
}

/* The slot the back control is injected into. display:contents so that an
   empty slot in the no-JS document generates no box at all, and therefore
   no stray flex gap in front of a title that has no button beside it. */
.tv-view-back {
  display: contents;
}

.tv-view-title {
  margin: 0;
  font-size: var(--tv-text-h1);
  font-weight: var(--tv-weight-display);
  line-height: var(--tv-leading-tight);
  letter-spacing: var(--tv-tracking-tight);
  text-wrap: balance;
}

.tv-live .tv-view {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--tv-hint-height);
  z-index: var(--tv-z-view);

  max-width: none;
  margin: 0;
  padding: var(--tv-space-12) var(--tv-rail) var(--tv-space-16);
  overflow-y: auto;
  overscroll-behavior: contain;

  background: var(--tv-ground);
  box-shadow: var(--tv-shadow-view);
}

.tv-live .tv-view.tv-open {
  display: block;
}

/* The view takes focus when it opens, and it draws NO ring, which is the one
   place in this theme where suppressing a focus outline is the correct call
   rather than the lazy one. Three reasons, and all three have to hold:

     The view is never user-focusable. It carries tabindex="-1" and is not in
     the tab order; a reader cannot arrive here by pressing anything. Focus is
     placed on it, once, by the script. That is the standard exception, and it
     is why every dialog implementation does the same.

     The feedback it would be duplicating is the entire screen changing. A
     ring exists to answer "where am I" on a surface with many places to be.
     A view has one place to be, and the reader just watched it arrive.

     Most importantly, it would spend the budget. This theme's whole argument
     is that the ring means "you are here" and is reserved for it. Drawn round
     the whole viewport it means nothing, and a mark that sometimes means
     nothing stops being trusted when it means something. Found by rendering
     the state and looking at it; the assertion suite was perfectly happy.

   Focus is not lost: the next Tab lands on the back control, which rings
   normally, and every interactive thing inside the view rings normally. */
.tv-live .tv-view:focus {
  outline: none;
}

/* Content inside a view is held to a measure even though the view is not,
   because a 62-character line does not become more readable on a bigger
   television. */
.tv-live .tv-view-inner {
  max-width: var(--tv-view-width);
  margin: 0 auto;
}

/* The back control is injected by shell.js, because a control that does
   nothing must not exist in a document where the script never ran. It
   carries a word as well as a mark: at ten feet, a lone glyph is a guess. */
.tv-back {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--tv-space-2);
  min-height: var(--tv-control-min);
  padding: var(--tv-space-2) var(--tv-space-4);

  font-family: var(--tv-font-sans);
  font-size: var(--tv-text-sm);
  font-weight: var(--tv-weight-medium);
  color: var(--tv-ink);
  background: var(--tv-surface);
  border: var(--tv-border-width) solid var(--tv-border-strong);
  border-radius: var(--tv-radius);
  cursor: pointer;
}

.tv-back:hover {
  background: var(--tv-surface-raised);
}

.tv-back:focus-visible {
  outline: var(--tv-ring-width) solid var(--tv-ring);
  outline-offset: var(--tv-ring-offset);
}

.tv-back > svg {
  width: var(--tv-icon-sm);
  height: var(--tv-icon-sm);
}

/* --------------------------------------------------------------------------
   HINT BAR
   The control legend, and the one piece of chrome that is on screen at
   every moment. As a page it is a footer stating what is true without a
   script: Tab and Enter. Live, shell.js rewrites the sentence to name the
   controls enhancement just added, and rewrites it again when a view opens,
   because the true sentence is different in the two states.

   The vocabulary is deliberately neutral. This shell names keys and
   actions; it never draws a lettered button in a circle, because the moment
   it does it stops being a dashboard for a room-scale screen and starts
   being an impression of somebody's console.
   -------------------------------------------------------------------------- */
.tv-hint {
  background: var(--tv-chrome);
  border-top: var(--tv-border-width) solid var(--tv-border-strong);
}

.tv-hint-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tv-space-4);
  min-height: var(--tv-hint-height);
  padding: var(--tv-space-2) var(--tv-rail);
}

.tv-hint-text {
  margin: 0;
  font-size: var(--tv-text-sm);
  color: var(--tv-ink-muted);
}

.tv-hint-key {
  color: var(--tv-ink);
  font-weight: var(--tv-weight-medium);
}

.tv-hint-end {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--tv-space-3);
}

.tv-clock {
  font-family: var(--tv-font-mono);
  font-size: var(--tv-text-sm);
  color: var(--tv-ink-muted);
}

/* A tray control: the hint bar's one square button, the ground picker that
   shell.js injects. */
.tv-tray-btn {
  display: grid;
  place-items: center;
  width: var(--tv-control-min);
  height: var(--tv-control-min);
  padding: 0;

  color: var(--tv-ink-muted);
  background: transparent;
  border: var(--tv-border-width) solid transparent;
  border-radius: var(--tv-radius);
  cursor: pointer;
}

.tv-tray-btn:hover {
  color: var(--tv-ink);
  background: var(--tv-surface);
  border-color: var(--tv-border);
}

.tv-tray-btn:focus-visible {
  outline: var(--tv-ring-width) solid var(--tv-ring);
  outline-offset: 0;
}

.tv-tray-btn > svg {
  width: var(--tv-icon-sm);
  height: var(--tv-icon-sm);
}

.tv-live .tv-hint {
  position: relative;
  z-index: var(--tv-z-chrome);
}

/* --------------------------------------------------------------------------
   THE INDEX
   The last block on the board, and the only navigation here that is not a
   picture. Rows of tiles are this archetype's whole idea and they are also
   its risk: a tile is a surface a reader has to recognise as navigation
   before it works, and the reader who does not recognise it has no way
   through the site at all. So the board ends with nine plain text links to
   the same nine screens. Class decision 44 in desktop-os-theme's
   docs/class-decisions.md is the rule and the argument.

   It is the same with the script and without it, because it is nothing but
   anchors. Live it is covered along with the rest of the board when a view
   opens, which is correct: the board is where you are when you are choosing
   where to go, and Esc or Back is what brings it back.

   DISTANCE-LEGIBLE, WHICH IS WHY THIS IS NOT SMALL PRINT. A text index in a
   ten-foot register cannot be a footer set in fine type. These are set at
   the body size, in full ink, on targets --tv-control-min tall, so they can
   be read and hit from where the tiles are read and hit. Full ink
   measures 17.31:1 on the ground and 14.94:1 on the hover face; muted ink
   would have been 8.74:1, which passes AA comfortably and still reads as a
   row of things that are switched off, which is the wrong thing for the one
   surface that has to look like navigation. The only thing quieter about an
   index item than a tile is that it has no face until you reach it.
   -------------------------------------------------------------------------- */
/* A rule above it, and room around the rule. The index is not another row
   of the board and should not read as one: everything above it is a picture
   you choose from, and this is the list of the same places in words. The
   divider is also what makes the bottom edge of the browse surface look
   deliberate at 1280, where the band scrolls and something has to be the
   thing that is half on screen. */
.tv-index {
  margin-top: var(--tv-space-8);
  padding-top: var(--tv-space-6);
  border-top: var(--tv-border-width) solid var(--tv-border);
}

.tv-index-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tv-space-2) var(--tv-space-3);
  margin: 0;
  padding: var(--tv-space-3) var(--tv-rail);
  list-style: none;
}

.tv-index-item {
  display: inline-flex;
  align-items: center;
  min-height: var(--tv-control-min);
  padding: var(--tv-space-2) var(--tv-space-4);

  font-size: var(--tv-text-body);
  font-weight: var(--tv-weight-medium);
  line-height: var(--tv-leading-snug);
  color: var(--tv-ink);
  text-decoration: none;

  border: var(--tv-border-width) solid transparent;
  border-radius: var(--tv-radius);
}

.tv-index-item:hover {
  background: var(--tv-surface);
  border-color: var(--tv-border);
}

.tv-index-item:focus-visible {
  outline: var(--tv-ring-width) solid var(--tv-ring);
  outline-offset: var(--tv-ring-offset);
}

/* --------------------------------------------------------------------------
   THE RESPONSIVE TRANSFORM
   Under 768px the ten-foot dashboard has to become something else, and the
   choice of what is argued in the README rather than assumed here. In
   short: rows become two-column grids and stack down the page. A single
   column would have been simpler and would have killed the feature this
   repo exists for, because with one tile per line the left and right arrow
   keys stop meaning anything. Two columns keeps all four directions live,
   and the spatial navigation in shell.js is written against geometry rather
   than against row indexes precisely so that it works here unchanged.

   The token overrides that come with this width live in tokens.css, next to
   the values they replace.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .tv-row-track {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin: 0;
    padding: var(--tv-space-3) var(--tv-rail);
  }

  .tv-live .tv-row-track {
    overflow-x: visible;
  }

  .tv-tile {
    width: auto;
  }

  /* A view on a narrow screen is the whole screen, and the reading measure
     is already the width of the device. */
  .tv-live .tv-view {
    padding: var(--tv-space-8) var(--tv-rail) var(--tv-space-12);
  }

  /* The hint bar's sentence and the clock stop fitting on one line long
     before this width. The clock goes; the sentence is the part that
     teaches the interface. */
  .tv-clock {
    display: none;
  }
}
