/* ------------------------------------------------------------------------
   SECTIONS. One block per station.

   The stations are numbered in the markup with data-station, 0 to 5, and
   they are the camera's stops in docs/design-direction.md section 3:

     0 hero      the corridor
     1 battle    the video in the app's own window frame
     2 camp      only real work makes power
     3 screens   the whole app is the game
     4 world     the Hollowlands
     5 pact      the waitlist

   Every one of them is transparent. Nothing here paints an opaque
   background, so when issue 3 puts its canvas in #stage the scene shows
   through the entire page without a single section needing to be changed.

   THE PIXEL RULE, in layout terms: every piece of art on this page is sized
   in whole-number multiples or whole-number divisions of its source file,
   and nothing is ever sized in %, vw or clamp(). The numbers are worked out
   in the comment next to each one.
   ------------------------------------------------------------------------ */

/* --- station 0: the hero --------------------------------------------------
   THE ONE THING THIS LAYOUT IS FOR: the copy lives in the top band and the
   party lives in the band under it, so the headline never sits on top of a
   hero's face. The two bands are set from real measurements, not by eye:

     the copy      about 520px tall at 1440x900, about 500 at 390x844
     the party     the 3D floor line is at 0.42 of the buffer height below
                   the middle, and a character's ink is 60 art pixels tall
                   sitting 10 above the bottom of its own 96px canvas
                   (measured with tools/ink.mjs)

   which lands the heads about 618px down at 1440x900 and about 636px down at
   390x844, both of them clear of the button. The floor number is in
   js/stage/camera-rig.js and the note there points back here.

   The hero is the one section that is NOT vertically centred, because centring
   it puts the copy straight through the middle of the party.
   ------------------------------------------------------------------------ */
.hero{align-items:flex-start;padding-top:var(--sp-7)}
@media (min-width: 1280px){
  .hero{padding-top:var(--sp-8)}
}

.hero-grid{display:flex;flex-direction:column}

/* The headline is the biggest thing that will ever be on this page, so it gets
   the whole column instead of the 18ch the other headings use. At 1280 and up
   that is 1056px, which holds it to three lines. */
.hero .display{max-width:none}

/* The four heroes, 96px canvases standing shoulder to shoulder. No gap: the
   character is drawn small on a big transparent canvas, so the canvases
   touching is what puts the party at the right spacing.
   96 at 1x on a phone, 96 at 2x above. Whole numbers, both.
   This is the fallback picture. Once the 3D corridor is up it is hidden and
   the same four stand in the scene instead. */
.hero .party{margin-top:var(--sp-7)}
.party ul{display:flex;gap:0;justify-content:center}
.party .idle{width:96px;height:96px}
.party img{width:100%;height:100%}
@media (min-width: 700px){
  .party .idle{width:192px;height:192px}
}

/* --- station 1: the battle ------------------------------------------------ */
.battle .battle-media{margin-top:var(--sp-7)}

/* The source is 1600x900. 800x450 is exactly half and 400x225 is exactly a
   quarter. Those are the only two sizes this video is ever shown at. Below
   440px even 400 plus the frame is wider than the screen, so the frame comes
   off there and the video runs to both edges of the band. */
/* max-width:none matters. The global max-width:100% would squash this to the
   viewport width on a phone, which is a fractional scale of a 1600px source
   and exactly what the whole pixel rule forbids. The .bleed band around it
   clips instead. */
.battle-video{width:400px;height:225px;max-width:none}

/* There are two of these in the markup: the one js/video.js fills in when the
   visitor gets near it, and a <noscript> copy that carries its own sources.
   With JavaScript off the first one would sit there for ever showing a poster,
   so it is taken out. The .js class is set by six lines in the head, before
   the first paint, so this never flashes. */
html:not(.js) [data-lazy-video]{display:none}

/* The frame is a box around the video, not a border on the video. box-sizing
   is border-box everywhere on this page, so the frame's own width is the
   video's width plus both borders and the video keeps its exact size. */
.video-frame{
  width:calc(400px + 2 * var(--frame-w));
  height:calc(225px + 2 * var(--frame-w));
  max-width:none;
}
@media (min-width: 900px){
  .battle-video{width:800px;height:450px}
  .video-frame{
    width:calc(800px + 2 * var(--frame-w));
    height:calc(450px + 2 * var(--frame-w));
  }
}
@media (max-width: 439px){
  .video-frame{border-width:0;width:400px;height:225px}
}

.battle .battle-cards{margin-top:var(--sp-7)}

/* --- station 2: the camp --------------------------------------------------
   THE ONE THING THIS LAYOUT IS FOR: the camp itself is the 3D behind the
   whole section and it fills the frame, so every word here sits inside one
   of the app's own window panels. That is not a scrim over the art, it is
   the app's own chrome over the app's own camp, which is how the game does
   it too. Copy laid straight on the clearing would not be readable and a
   veil over the clearing would waste the art.

   The section owns TWO screens of scroll. The three panels are spread down
   them with space-between, so the argument is on the first screen, the
   ledger of real work crosses the join, and the last panel is at the bottom
   of the second screen. By then the camp is unobstructed and the fire is
   going out. The drain beats in js/stage/camp.js are timed to that.
   ------------------------------------------------------------------------ */
.camp-section{align-items:stretch;min-height:200svh}

/* A phone gets more scroll than a desktop, which is the opposite of the usual
   rule, and it is because of what the panels cover. A panel is nearly the full
   width of a phone, so the only camp a visitor sees is what shows between two
   of them, and space-between only opens that gap up if the section is taller
   than the panels stacked. At 260svh the gap is about 400px, which is half a
   phone screen of clearing with nothing on top of it. On a desktop the panels
   are half the width and the camp is never hidden, so 200svh is plenty. */
@media (max-width: 699px){
  .camp-section{min-height:260svh}
}

.camp-flow{
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  gap:var(--sp-8);
}

.camp-panel,
.camp-ledger{
  width:100%;
  max-width:560px;
  padding:var(--sp-6);
}
.camp-panel > * + *{margin-top:var(--sp-5)}

/* The last panel changes side, so the two characters nearest the camera are
   not behind a panel for the whole section. */
@media (min-width: 900px){
  .camp-panel--end{align-self:flex-end}
}

/* --- the ledger: real work in, power out ---------------------------------
   One row per character: the portrait, who they are, the real work that
   charges them, and that character's meter. The meters drain on the same
   scroll position as the camp behind them.
   ------------------------------------------------------------------------ */
.camp-ledger > * + *,
.ledger-grid > div > * + *{margin-top:var(--sp-5)}

.charge{
  display:grid;
  grid-template-columns:56px minmax(0,1fr);
  gap:var(--sp-4);
  align-items:center;
}
.charge + .charge{margin-top:var(--sp-4)}

/* A 96px sprite canvas with a 52x60 drawing in it, cropped to the drawing by
   a window and a whole pixel offset. Cropping is not scaling: every pixel is
   still exactly one pixel. The numbers are from tools/ink.mjs. */
.portrait{
  display:block;
  width:56px;
  height:64px;
  overflow:hidden;
}
.portrait img{
  width:96px;
  height:96px;
  max-width:none;
  margin:-24px 0 0 -24px;
}

.charge-body{display:block}
.who{
  display:block;
  font-size:var(--f-h3);
  line-height:var(--lh-h3);
  color:var(--ink-bright);
}
.tasks{
  display:block;
  font-size:var(--f-small);
  line-height:var(--lh-small);
  color:var(--ink-muted);
}

/* The meter. A whole number of pixels wide, so the fill can be a whole
   number of pixels too and its edge never lands between two of them. */
.meter{
  display:block;
  width:176px;
  height:16px;
  margin-top:var(--sp-2);
  background:var(--panel);
  box-shadow:inset 0 0 0 2px var(--border-active);
}
.meter-fill{
  display:block;
  width:176px;
  height:100%;
  background:var(--accent);
}
@media (min-width: 700px){
  .meter,.meter-fill{width:264px}
}

.camp-hint{text-align:left}

/* The real habits screen, as proof that the work is real work. 800x1732 is
   the app's 400x866 grid at 2x, so 200x433 is exactly a quarter of it. The
   same size the strip in station 3 uses, and for the same reason.

   It is the ledger's second column above 900px and it is not there at all
   below that: a phone shows the whole camp in the frame already, and 433px
   of screenshot on top of that is 433px of scrolling nobody asked for. */
.camp-proof{display:none}
/* content-box, and it is not a detail. box-sizing is border-box everywhere on
   this page, so the 2px border was coming out of the picture and the 800x1732
   file was being drawn at 196x429. That is a 0.245 scale of the source and it
   is the one thing the whole site forbids. The picture is exactly a quarter,
   200x433, and the border sits outside it. Found by Lighthouse in issue 9. */
.camp-proof img{
  box-sizing:content-box;
  width:200px;
  height:433px;
  border:2px solid var(--border-active);
}
.camp-proof figcaption{margin-top:var(--sp-3)}

@media (min-width: 900px){
  .camp-ledger{max-width:720px}
  /* The screenshot is 433px tall and the four rows next to it are shorter, so
     the rows sit in the middle of it rather than leaving a hole at the bottom
     of the panel. */
  .ledger-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-6)}
  .ledger-grid > div{align-self:center}
  .camp-proof{display:block}
}

/* --- the camp as two flat screenshots ------------------------------------
   The no-JavaScript, no-WebGL version of the section. camp_screen.png is
   400x748, or exactly half that on a small phone.
   ------------------------------------------------------------------------ */
.camp{
  display:flex;
  flex-wrap:wrap;
  gap:var(--sp-5);
  justify-content:center;
}
.camp figure{margin:0}
.camp figcaption{margin-top:var(--sp-3)}
.camp img{width:400px;height:748px}

/* The stacked state, which only exists when the scroll engine is running but
   the 3D camp is not. The two camps sit on top of each other and the scroll
   position wipes between them. */
.camp.is-stacked{
  display:block;
  position:relative;
  width:400px;
  height:748px;
  margin:0 auto;
}
.camp.is-stacked figure{position:absolute;inset:0}
.camp.is-stacked figcaption{display:none}
.camp.is-stacked [data-camp="spent"]{opacity:0}

@media (max-width: 439px){
  .camp img{width:200px;height:374px}
  .camp.is-stacked{width:200px;height:374px}
}

/* --- station 3: the screens ----------------------------------------------
   THE ONE THING THIS LAYOUT IS FOR: the six app screens stand in the 3D
   scene at the app's own 400x866 grid, one buffer texel per art pixel, so
   the one at the front is wider than half a desktop frame and fills a
   phone. Copy cannot sit on top of that and be read, so it sits in one of
   the app's own window panels, exactly as station 2 does.

   THE HEIGHT IS SET BY THE TURN, not by the copy, and here is the sum. The
   section carries three things one after another: the panel, which is
   about 600px tall and has to be out of the frame before the turning
   starts; six screens, each needing about 270px of scrolling to stand
   still in long enough to be looked at; and one more screenful, because a
   station's scroll runs on while the next section climbs the frame and the
   slabs have to be gone before the Hollowlands arrive. 600 + 1350 + 900 is
   320svh at 1440x900. The beats in js/stage/screens.js are fractions of
   this number, so moving it means moving those.

   IT IS ONLY THERE WHEN THE SLABS ARE. All that scroll exists to turn six
   screens in the 3D scene, and with no WebGL the section is a panel and a
   strip of six screenshots, which is one ordinary screen's worth. Two
   empty ones under it would be a bug. html.stage-on goes on at boot,
   before anybody has scrolled, and ScrollTrigger is refreshed after it.
   ------------------------------------------------------------------------ */
.screens{align-items:stretch}
html.stage-on .screens{min-height:320svh}

.screens-flow{
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  gap:var(--sp-8);
}

.screens-panel{
  width:100%;
  max-width:560px;
  padding:var(--sp-6);
}

/* "Keep scrolling and they turn" is only true when there are slabs to turn.
   With no WebGL the section is the strip, and the strip does not turn. */
.screens-hint{display:none}
html.stage-on .screens-hint{display:block}

/* --- the labels that ride with the slabs ---------------------------------
   One per screen, in the app's own type and its own window frame, moved by
   js/stage/screens.js. They are fixed to the viewport because the canvas
   is, and they are the only text on this page that is positioned against
   the buffer's own grid rather than against the document.

   They start invisible and the stage turns them on. With no JavaScript
   there is nothing to position them, so the markup is hidden outright and
   the strip of six screenshots does the job instead.
   ------------------------------------------------------------------------ */
.screen-labels{
  position:fixed;
  inset:0;
  z-index:2;
  pointer-events:none;
}

.screen-label{
  position:absolute;
  top:0;
  left:0;
  opacity:0;
  white-space:nowrap;
  font-size:var(--f-ui);
  line-height:var(--lh-ui);
  color:var(--ink-bright);
  padding:var(--sp-2) var(--sp-4);
}

/* --- the strip: the no-WebGL version of the section ----------------------- */
/* The screen files are 800x1732, which is the app's own 400x866 grid at 2x.
   200x433 is exactly a quarter of that, and it is the same on a phone: 1732
   does not divide by 8, so there is no smaller whole-number size. */
.strip{
  display:flex;
  gap:var(--sp-4);
  padding-bottom:var(--sp-4);
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  overscroll-behavior-x:contain;
}
.screens .strip{margin-top:var(--sp-7)}
.strip li{flex:0 0 auto;scroll-snap-align:center}
.strip img{width:200px;height:433px;border:2px solid var(--border-active)}
.strip figcaption{margin-top:var(--sp-2)}

/* --- station 4: the Hollowlands ------------------------------------------
   THE ONE THING THIS LAYOUT IS FOR: the party, the horde and the chieftain
   are in the 3D scene at twice the size anything else on the stage is drawn
   at, so the boss's drawing is 246 of a 300 texel frame. Copy cannot sit on
   open art at that size and be read, so it sits in one of the app's own
   window panels, the same call stations 2 and 3 both made.

   THE HEIGHT IS THREE SCREENS AND THE PANELS DIVIDE IT. Each panel sits at
   the TOP of its own screen of the section, which puts panel 1 in the frame
   from 0 to about 0.11 of the station, panel 2 from 0.11 to 0.33, and panel
   3 from 0.44 to 0.67. The beats in js/stage/hollowlands.js are lined up
   under them: the party under panel 1, the horde under panel 2, the
   chieftain under panel 3. Move this number and those move with it.

   Three screens is also what the last beat needs. A station's scroll runs
   until its bottom reaches the top of the window, so station 5's top
   crosses the bottom of the window at 0.67 and has climbed the whole frame
   by 1.0. The boss gets 0.52 to 0.68 to himself and is gone before a word
   of the Pact is readable.

   IT IS ONLY THERE WHEN THE SCENE IS. With no WebGL the section is three
   panels and a row of sprites, which is one ordinary screen's worth, and
   two empty screens under it would be a bug.
   ------------------------------------------------------------------------ */
.world{align-items:stretch}
html.stage-on .world{min-height:300svh}

.world-flow{display:flex;flex-direction:column}

/* One screen of scroll each, with the panel at the top of it. Centred, a
   panel lands on the party's heads and on the chieftain's face: the art
   stands on the floor line, which is in the lower half of the frame, so
   the copy belongs in the upper half. */
.world-slot{display:flex;align-items:flex-start}
html.stage-on .world-slot{min-height:100svh}

/* With no scene there is no screen of scroll between two panels, so they need
   a gap of their own or they sit on top of each other. With the scene there is
   a whole screen and a gap would push every beat down by a fraction. */
.world-slot + .world-slot{margin-top:var(--sp-7)}
html.stage-on .world-slot + .world-slot{margin-top:0}

/* A phone's frame is 844 texels tall and the floor line is 363 of them below
   the middle, so the party stands in the bottom quarter and the first panel at
   the very top leaves half a screen of dark between the two. It is the first
   thing the section shows, so the panel comes down to meet the art. Only the
   first: panel 3 is over the chieftain's own half of the frame on a phone and
   moving it down puts it on his face. */
@media (max-width: 899px){
  html.stage-on .world-slot:first-child{padding-top:16svh}
}

.world-panel{
  width:100%;
  max-width:520px;
  padding:var(--sp-6);
}
.world-panel > * + *{margin-top:var(--sp-5)}

/* The last panel changes side, so it is not in front of the chieftain's own
   half of the frame. His drawing is 206 texels wide in the middle of a 480
   texel frame, which leaves a wing either side and this sits in one.

   margin-left, NOT align-self. A .world-slot is a flex ROW, so align-self
   moves an item up and down rather than left and right, and align-self:
   flex-end put this panel at the BOTTOM of its screen, a third of a station
   later than the beat it belongs to. */
@media (min-width: 900px){
  .world-panel--boss{margin-left:auto;max-width:440px}
}

/* --- the horde as a still row: the no-WebGL version of the section -------- */
/* The goblins are 72px canvases, the chieftain is 192px. 1x on a phone and
   2x above, for both. */
.world-fallback{margin-top:var(--sp-8)}
.world .horde{margin-top:var(--sp-7)}
.horde ul{display:flex;gap:0;justify-content:center;align-items:flex-end}
.horde .idle{width:72px;height:72px}
.horde img{width:100%;height:100%}

.boss{margin-top:var(--sp-5);display:flex;justify-content:center}
.boss .idle{width:192px;height:192px}
.boss img{width:100%;height:100%}

@media (min-width: 700px){
  .horde .idle{width:144px;height:144px}
  .boss .idle{width:384px;height:384px}
}

/* --- station 5: the Pact -------------------------------------------------
   THE ONE THING THIS LAYOUT IS FOR: the camp is behind the whole section at
   one texel per art pixel (js/stage/pact.js), so every word sits inside one
   of the app's own window panels. Same call as stations 2, 3 and 4.

   On a wide screen the panel sits on the LEFT and stops at 520px, so the
   fire is not behind it. The fire is at art x 212 of the camp's own 400 wide
   grid, which is 12 texels right of the middle of the frame, and the flare
   is the reward for signing: it has to be visible from where the button is.
   On a phone the panel is the width of the screen and the camp shows above
   and below it, which is the only way round that works at 390px.
   ------------------------------------------------------------------------ */
.pact-panel{
  width:100%;
  max-width:520px;
  padding:var(--sp-6);
}
@media (min-width: 900px){
  .pact-flow{display:flex}
  .pact-panel{margin-right:auto}

  /* With no scene there is nothing in the space the panel is standing out of
     the way of, so it takes the middle instead. That is reduced motion, no
     WebGL and no JavaScript at all, which are the three cases that never get
     the class. */
  html:not(.stage-on) .pact-panel{margin-left:auto}
}

/* On a phone the panel is nearly the width of the screen, so the only camp
   anybody sees is the strip under it. The panel goes to the TOP of the
   section and keeps its padding small, which leaves about 290px at the
   bottom, and js/stage/pact.js puts the fire in exactly that strip. Centred
   instead, the panel sits on the fire and the flare is invisible on the one
   device most visitors are on. */
@media (max-width: 899px){
  .pact{align-items:flex-start;padding-top:var(--sp-5)}
  .pact-panel{padding:var(--sp-5)}
}

/* A little more air above the form than the stack gives, because the label
   above it is pixel type and pixel type needs room. */
.pact .pact-form{margin-top:var(--sp-6)}

/* The stamp is only ever shown once .pact-ask has been hidden, so it is the
   panel's only child by then and it carries no margin of its own. */
.pact .stamp{margin:0}

/* --- the footer -----------------------------------------------------------
   THE ONE OPAQUE THING ON THE PAGE, and issue 8 made it that way. Every
   section is transparent so the scene shows through the whole document, but
   the footer is not a station: it is the end of the page, and from issue 8
   the camp is lit behind it at full brightness. Two lines of small print on
   a lit clearing cannot be read. A solid void band with the frame's own
   hairline over it is how the app ends a screen too. */
.site-footer{
  position:relative;
  background:var(--void);
  border-top:2px solid var(--border-quiet);
  padding:var(--sp-7) 0;
}
.site-footer p + p{margin-top:var(--sp-2)}
