/* ==========================================================================
   FÖRLOPPET — the film hero for Restaurang Moldau

   THE FILM PLAYS. It is not scrubbed by scroll any more, and that is the whole
   change. The scrub was rebuilt twice and measured green both times -- every
   frame owned, every frame present, 16.7 ms median in real Chrome with no long
   tasks -- and it still read as stutter, because the defect was never a
   dropped frame. 100 frames over 1800 px of reel means an ordinary trackpad
   flick plays 42 seconds of footage in under a second. A fast-forward looks
   like a fast-forward however perfectly it is painted, and a film that runs at
   the speed of somebody's thumb cannot be cinematic: a shot has its own
   timing, and giving that timing away is giving away the thing that makes it
   a film.

   So the reference wins the argument (jolesch.de/en/schnitzel, measured rather
   than admired: 41.92 s, 1280x720, 11.3 MB, muted, autoplay, loop, and almost
   no interface). Ours: 41.83 s of the same kitchen, playing itself, with the
   page's furniture keeping out of its way. The browser composites a <video>
   off the main thread, so this cannot stutter for any reason we control.

   THE TYPE HAS NO GROUND OF ITS OWN, by decision on 2026-08-10, and the picture
   carries a small fade at its bottom instead. Four things were built that day and
   three were rejected as colour over the film: a heavy fade to 0.94, a full width
   band, a plaque hugging the words. What stands is the type lying straight on the
   film with the shot's own bottom darkened to --fb-fade -- a grade on the
   picture, not a layer under the words. The cost is measured and it is real:
   the brightest ~32px patch in the bottom 40 % of the frame is 254/255 (f-077,
   the plate under the lift) and cream on that is 1.13:1 against a 4.5:1
   requirement. Moving the type does not save it -- in an 8x5 grid over the whole
   picture the darkest cell across all 100 frames still peaks at 139, and cream
   needs 111 or below. Both numbers come from mat_flor() in tools/hero_film.py,
   which owns them; do not restate them elsewhere.

   So: legible through the dark two thirds of the film, thinning out when the
   fryer flares. That is Dennis's call made with the numbers in front of him, and
   it is recorded here so it is not relitigated every time someone reads the
   file.

   Gold is never on the film. Measured before, still true: at any usable veil
   it tops out at 4.5:1 and falls to 1.06:1 on a light patch. On film the ink
   is cream.

   Sections: 1 the film  2 the type on it  3 the beat rail
             4 the ledger (takterna)  5 takt 06 (the room)
             5b takt 07 (the complaints)  6 degradation
   ========================================================================== */

/* 1. The film =========================================================== */

.fb {
  position: relative;
  display: grid;
  /* One screen minus the sticky header, measured by the script rather than
     assumed: the header rewraps on a language switch, which fires no resize. */
  min-height: calc(100svh - var(--fb-topp, 8rem));
  background: var(--valnot);
  color: var(--papper);
  overflow: clip;
  --fb-guld-linje: rgb(201 162 39 / 0.55);
}

.fb__duk { grid-area: 1 / 1; position: relative; }

/* A SMALL FADE, AND IT BELONGS TO THE PICTURE. Dennis asked for one back after
   seeing the type with nothing behind it at all.

   Two things make it different from the one that was rejected this morning.
   It ends at --fb-fade rather than 0.94, so it is a darkening of the bottom of
   the shot and not a curtain. And it lives on the FILM's layer, ramping over the
   bottom of the picture, instead of on the text block: the old one was the
   text block's own background bled sideways with inset:0 -50vw, which pushed the
   block 592 px past its own section and was what the gate's clip pass kept
   reporting. Tied to the picture there is nothing to bleed and no edge to see --
   a long ramp from zero has no start.

   What it buys is one number to turn, --fb-fade, and mat_flor() prints the whole
   ladder against the film's brightest patch. It sat at 0.45 for an afternoon,
   which read as 2.51:1 -- better than bare film and short of the requirement --
   and Dennis then asked for it stronger. At 0.65, sampled on the rendered worst
   frame rather than from the formula: the address reads 10.06:1, the wordmark
   6.58:1 where it crosses the crumb, and 2.67:1 where it crosses the white plate
   at bottom left -- up from 1.13:1 bare. That last spot is still under 4.5:1 and
   0.72 would clear it. Do not write "compliant" here: the plate is effectively
   pure white in that frame, and cream on white cannot be rescued by a veil that
   Dennis would accept as a fade.

   NO z-index ON THIS, and the reason is worth the paragraph because getting it
   wrong broke the title card. The poster and the video are absolutely positioned
   children of this same box, so the instinct is to raise the gradient above them
   -- but positioned siblings at z-index auto paint in tree order, and an ::after
   is the last child there is. It already wins. Adding z-index 1 made it outrank
   .fb__inre as well (position:relative with z-index auto opens no stacking
   context), so the fade painted over the words: the Boka bord button rendered
   (142,38,49) against its palette red near (200,16,46).

   Lifting .fb__inre to match then broke something further away, which is the
   real lesson. .fb__marke carries z-index 3 to rise above the title card's 2
   while it is lifted into it -- and a stacking context on .fb__inre traps that 3
   inside it, so the wordmark vanished for the first seconds of the film. Two
   elements deep, invisible in a screenshot of the resting state, and caused
   entirely by a z-index that was never needed. */
.fb { --fb-fade: 0.65; }

.fb__duk::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  /* THE RAMP HAS TO BE FULL WHERE THE LETTERS ARE, not at the bottom edge. The
     first version reached --fb-fade at 100 % and put its softest part exactly
     where the wordmark sits: sampled, the mark stood on about 0.14 while the
     file claimed 0.65, so the measured ratio described empty picture below the
     type. Same class of mistake as writing a veil's stops in percentages instead
     of lengths, which this file already carries a note about.
     So: the box covers the bottom 75 %, the ramp finishes at 40 % of it -- which
     is where the mark's top edge falls -- and holds --fb-fade from there down.
     Every glyph is on the full value, and the 300-odd px of ramp above it is
     what keeps the whole thing edgeless. */
  height: 75%;
  background: linear-gradient(
    to bottom,
    rgb(43 26 14 / 0) 0,
    rgb(43 26 14 / calc(var(--fb-fade) * 0.55)) 24%,
    rgb(43 26 14 / var(--fb-fade)) 40%,
    rgb(43 26 14 / var(--fb-fade)) 100%);
  pointer-events: none;
}

.fb__ruta,
.fb__film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
}

/* The poster is the hero until the film is actually playing -- not until it
   has loaded, not until it has been asked to play. Under reduced motion or a
   metered connection the script never attaches a source and the poster is the
   whole hero, which is why it is frame 3 and not frame 1: frame 1 is the tail
   of a fade from black, mean luminance 24.7 of 255 against 102.1. */
.fb__film { opacity: 0; transition: opacity 900ms ease; }
.fb[data-film="spelar"] .fb__film { opacity: 1; }
.fb[data-film="spelar"] .fb__ruta { opacity: 0; transition: opacity 900ms ease; }

/* 1b. The title card =================================================== */

/* A film gets a title before it starts. The card is opaque, so it is also the
   thing that hides the first second of buffering -- the film is loaded behind
   it and only asked to play when the card is done, which means the visitor
   never sees a stalled first frame and never misses the opening blow.

   Contrast is trivial here and stated anyway, because a card that is measured
   cannot drift: --guld-ljus on --valnot is 10.26:1, --tidning on --valnot is
   11.37:1. Nothing sits on the film.

   Only opacity and transform are animated. Growing the letter-spacing would
   have been the obvious move and it relayouts the line on every frame of the
   animation; the same "settling into place" reads fine from a 6px rise. */
.fb__intro {
  grid-area: 1 / 1;
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  padding: var(--s-5);
  text-align: center;
  background: #1b1008;                          /* one step under --valnot */
  transition: opacity 900ms ease;
}

.fb__intro-inre {
  margin: 0;
  display: grid;
  justify-items: center;
  /* Not one gap for four different joins. The mark and "presents" belong
     together, "presents" and the title are the beat of the card, and the rules
     stand off from all of it. */
  gap: var(--s-2);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 1100ms ease, transform 1100ms ease;
}
.fb[data-intro="visar"] .fb__intro-inre { opacity: 1; transform: none; }

/* The wordmark, then "presents", then the title: a film's own title card. The
   mark is the same 7 KB path data as everywhere else, masked over cream, so
   there is one file for every place the wordmark appears. */
/* A MASK CLIPS ITS DESCENDANTS, NOT JUST THE ELEMENT'S OWN PAINT. The heart
   started out as a child of the masked span and was invisible: it sits outside
   the script's silhouette, so the mask cut it away. It has to be a SIBLING of
   the masked box, inside a plain wrapper that positions both. The wrapper also
   carries the room the heart needs below the script -- the way it hangs in
   their print and in the site header -- or the heart lands on "presents". */
.fb { --fb-marke-b: clamp(10rem, 30vw, 17rem); }

/* An h1 since 2026-08-07, so the browser's own heading margins and 2em font
   size have to be shown the door: the box is sized off --fb-marke-b and the
   only text inside it is visually hidden. */
.fb__marke {
  position: relative;
  display: block;
  margin-top: 0;
  font-size: 1rem;
  width: var(--fb-marke-b);
  /* The room the heart needs scales with the mark, or it grows into
     "presents" the moment the mark does. 0.214 is what the heart now hangs
     below the mark's box (13.25 % of the width, at the `top` further down) plus
     the 8.1 % of air that 0.19 left under it before the heart moved down. */
  margin-bottom: calc(0.214 * var(--fb-marke-b));
}

.fb__marke-bild {
  display: block;
  aspect-ratio: 181 / 79;
  background: var(--papper);                    /* 14.6:1 on the card */
  -webkit-mask: url("img/mark-ordmarke.svg") no-repeat center / contain;
          mask: url("img/mark-ordmarke.svg") no-repeat center / contain;
}

/* THE MARK BOUNCES IN. Squash and stretch, the oldest rule in animation: a
   thing that bounces is wider and shorter when it lands, taller and thinner on
   the way up. Two landings, each softer than the last, and it settles.

   It sits on the wrapper rather than the mark, so the heart rides along with
   it, and it is scale and translate only -- the compositor's own two
   properties, no layout and no paint per frame. The earlier proposal animated
   mask-size, which is neither.

   THE SETTLED STATE IS THE DEFAULT and the animation starts from above it, not
   the other way round. Anything that stops the animation running -- an old
   engine, a tab that never painted -- then leaves the mark where it belongs.
   It has to fail towards being there. */
.fb__marke-inre { display: block; transform-origin: 50% 100%; }

.fb[data-intro="visar"] .fb__marke-inre,
.fb[data-intro="start"] .fb__marke-inre {
  animation: fb-studs 1250ms cubic-bezier(0.3, 0.7, 0.4, 1) backwards;
}
@keyframes fb-studs {
  0%   { opacity: 0; transform: translateY(-90%) scaleY(1.10) scaleX(0.94); }
  22%  { opacity: 1; transform: translateY(0) scaleY(0.80) scaleX(1.14); }
  38%  { transform: translateY(-34%) scaleY(1.07) scaleX(0.96); }
  56%  { transform: translateY(0) scaleY(0.90) scaleX(1.07); }
  72%  { transform: translateY(-12%) scaleY(1.03) scaleX(0.98); }
  86%  { transform: translateY(0) scaleY(0.97) scaleX(1.02); }
  100% { opacity: 1; transform: none; }
}

/* The heart is the one part of the mark that is NOT recoloured: it carries the
   Austrian flag, and a flag whose red is a token is not that flag. That is also
   why the mark is a mask plus an image rather than one SVG -- a mask paints a
   single colour, so the flag has to live outside it.

   It lands after the mark has taken its first bounce and bounces once more on
   its own, so the logo does not arrive as one object. Same rule again: landed
   is the default. */
.fb[data-intro="visar"] .fb__marke-hjarta,
.fb[data-intro="start"] .fb__marke-hjarta {
  animation: fb-hjarta-studs 700ms cubic-bezier(0.3, 0.7, 0.4, 1) 620ms backwards;
}
@keyframes fb-hjarta-studs {
  0%   { opacity: 0; transform: translateY(-160%) scale(0.8); }
  40%  { opacity: 1; transform: translateY(0) scale(1.12); }
  62%  { transform: translateY(-30%) scale(0.96); }
  82%  { transform: translateY(0) scale(1.04); }
  100% { opacity: 1; transform: none; }
}

.fb__marke-hjarta {
  position: absolute;
  left: 36.8%;
  /* Below the script, not inside it. At 76 % it sat behind the letters and read
     as a smudge on the "d"; their print and the site header both hang it clear
     of the baseline.

     THE GAP IS MEASURED OFF THEIR OWN LOGO, not judged by eye. In
     mark-ordmarke-384.webp -- the client's lockup, script and heart together --
     the heart's body starts 3.16 % of the wordmark's width below the script's
     lowest ink in those same columns. At 92 % ours sat at 0.79 %, a quarter of
     that, and read as stuck to the "o". 97.4 % puts the body's top edge on
     their measure. Note the units differ: `top` is a share of the mark's HEIGHT
     (0.4365 of its width), so 3.16 % of the width is 5.4 points of `top`. */
  top: 97.4%;
  width: 12.5%;
  height: auto;
}

/* Lowercase italic, the way a studio credit is set -- and small, because it is
   the one word on the card that is not the point. */
.fb__intro-presents {
  margin-bottom: 0.2rem;
  font-family: var(--f-bok);
  font-style: italic;
  font-size: clamp(0.95rem, 0.8rem + 0.5vw, 1.25rem);
  letter-spacing: 0.02em;
  color: var(--tidning);                        /* 11.37:1 on the card */
}

.fb__intro-titel {
  font-family: var(--f-bok);
  font-weight: 400;
  font-size: clamp(1.6rem, 1rem + 3.4vw, 4rem);
  line-height: 1.1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--guld-ljus);                      /* 10.26:1 */
  text-wrap: balance;
}

.fb__intro-under {
  font-family: var(--f-display);
  font-size: var(--t-xs);
  letter-spacing: var(--spar-vid);
  text-transform: uppercase;
  color: var(--tidning);                        /* 11.37:1 */
}

.fb__intro-regel {
  margin-block: var(--s-2);
  display: block;                               /* a span with a size is nothing without this */
  width: min(9rem, 40vw);
  height: 1px;
  background: var(--guld);
  opacity: 0.7;
}

/* Gone, and out of the way of clicks and the tab order. */
.fb[data-intro="klar"] .fb__intro {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 900ms ease, visibility 0s linear 900ms;
}

/* No card at all when the film is not going to play: under reduced motion, on
   a metered connection, and without JavaScript. In all three the poster is the
   hero, and a title card in front of a still is a door with nothing behind it.
   data-intro is only ever set by the script. */
.fb:not([data-intro]) .fb__intro { display: none; }

/* 2. The type on it ===================================================== */

/* A FOOTER, NOT A COMPOSITION. The first attempt put the eyebrow, a huge
   wordmark, a paragraph, the rail, the clock and the buttons over the film,
   and the veil that made all of it legible dimmed the plate at 0:41 -- the one
   shot the whole hero exists for. The reference does the opposite: type at the
   edges, picture in the middle, and the restaurant's name is in the header
   where it already was. So the wordmark is gone from here (it is 60 px away in
   the header, and repeating it twice on one screen was never an argument), and
   what remains is one line about what you are watching, the way to book, and
   the film's own chapters. The film keeps its top two thirds untouched. */
.fb__inre {
  grid-area: 1 / 1;
  position: relative;
  align-self: end;
  width: min(100% - 2 * var(--s-4), var(--matt));
  margin-inline: auto;
  padding: 7rem 0 var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

/* Above the veil, which is the ::before below. */
.fb__inre > * { position: relative; }

/* NO GROUND AT ALL. Dennis, 2026-08-10, after the fade, a band and a plaque
   were each rejected as colour over the picture: as it was, and no fade. So the
   type lies straight on the film with nothing behind it.

   What that costs, measured rather than guessed, so the next reader does not
   have to rediscover it: over all 100 sampled frames the brightest patch in the
   bottom of the frame reaches 254 of 255, and cream on that is 1.13:1 where
   4.5:1 is required. In an 8x5 grid across the whole picture the darkest cell
   still peaks at 139, and cream needs 111 or below, so no placement rescues it
   either -- see mat_flor() in tools/hero_film.py for both numbers. The hero
   therefore reads well through the dark two thirds of the film and the wordmark
   thins out when the fryer flares. This is Dennis's call, made with the numbers
   in front of him, and it is written down here rather than argued again.

   The 7rem of top padding stays: it is what holds the block low in the frame,
   and it was only ever the fade's runway by coincidence. */

.fb__rad {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4) var(--s-6);
}

/* The address, between the mark and the button. This gold used to have two
   different ratios depending on how high in the block it sat, because the veil
   it stood on changed alpha with height. On the opaque band there is one number
   for it wherever it goes, which is why the position is now a composition
   question and not a contrast one. The top margin clears the room the mark
   reserves under itself for the heart. */
.fb__ogonbryn {
  margin: var(--s-2) 0 var(--s-6);
  font-family: var(--f-display);
  font-weight: 500;
  font-size: var(--t-xs);
  letter-spacing: var(--spar-vid);
  text-transform: uppercase;
  color: var(--guld-ljus);
}

.fb__knappar { margin: 0; flex: none; }
.fb .knapp--linje { color: var(--papper); border-color: var(--papper); }
.fb .knapp--linje:hover { background: var(--papper); color: var(--valnot); }
.fb :focus-visible { outline-color: var(--guld-ljus); }

/* A phone is one column. There is no gradient left to re-aim at the first line
   -- an opaque ground needs no run-up -- so only the spacing changes here. */
@media (max-width: 47.99rem) {
  .fb__inre { padding-top: 5.5rem; gap: var(--s-3); }
  .fb__rad { align-items: flex-start; }
  .fb__knappar { width: 100%; }
}

/* 3 and 4 are GONE ====================================================== */

/* The beat rail in the hero and the five-row ledger under it were removed on
   2026-08-05, with the running clock. Roughly 220 lines: .fb__takter, .fb__prick,
   .fb__urverk, .fb__klocka, .fb-ledger and everything under it. They are at
   9225269. Nothing is commented out here, because commented-out CSS is a
   promise nobody keeps -- and the phone rules that used to hide the rail went
   with it, so the mobile block below is shorter than it was for a reason.

   The two sections that remain still number themselves 06 and 07, which now
   count beats the visitor never sees. Flagged for Dennis, not renumbered
   quietly. */

/* 5. Takt 06 — the room, numbered into the same ledger ================== */

.fb-rum__rutnat {
  display: grid;
  gap: var(--s-6);
  align-items: start;
}
@media (min-width: 52rem) {
  .fb-rum__rutnat { grid-template-columns: 1fr 1fr; gap: var(--s-7); }
}
.fb-rum__nr {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: var(--t-xl);
  line-height: 1;
  color: var(--guld-mork);                       /* 6.12:1 on papper */
  margin: 0 0 var(--s-2);
  font-variant-numeric: tabular-nums;
}
/* 06 sits on --papper, where --guld-mork is the right gold at 6.12:1. The same
   numeral class is reused for 07 inside the complaint band, which is --valnot,
   and there that gold measures 2.39:1 and fails. Gold as text on a dark ground
   is --guld-ljus (8.14:1). Scoped rather than changed on the shared rule, which
   would break 06 -- one gold cannot do two jobs. */
.fb-klagomal .fb-rum__nr { color: var(--guld-ljus); }

.fb-rum__bild { position: relative; }
/* Their own axe, beside their own sentence about the furniture being cut with
   an axe. Evidence for the line, not a sticker on the corner. */
.fb-rum__yxa {
  position: absolute;
  /* Inside the figure's own box on the inline axis. A negative `right` here put
     4-7px of axe past the viewport at every width below 1024px, which the
     320-1920 sweep caught immediately. Vertical overhang is free. */
  right: 0.25rem;
  bottom: -2.25rem;
  width: min(11rem, 34%);
  transform: rotate(-9deg);
  filter: drop-shadow(0 8px 14px rgb(43 26 14 / 0.35));
}

/* 5b. Takt 07 — the complaints department =============================== */

/* Carried over from direction C, where the gilt oval is the whole idea: a
   restaurant hangs its founder or its royal warrant in one, and Moldau's has a
   rubber duck in a Tyrolean hat in it. It is numbered into the ledger here as
   the seventh beat, because in this direction the sections ARE the film's
   beats and a section that started its own numbering would read as a different
   page.

   --fb-guld-linje used to be declared on .fb, which is this section's SIBLING
   and not its ancestor, so the hairline it draws never rendered at all. */
.fb-klagomal { --fb-guld-linje: rgb(201 162 39 / 0.55); }

.fb-klagomal { background: var(--valnot); color: var(--papper); }

.fb-klagomal__inre {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: var(--s-7);
  align-items: center;
}

.fb-klagomal__lapp {
  margin: var(--s-5) 0 0;
  padding-left: var(--s-5);
  border-left: 3px solid var(--guld);
  font-family: var(--f-hand);
  font-size: var(--t-lg);
  line-height: 1.35;
  color: var(--guld-ljus);                      /* 10.26:1 on valnot */
}
.fb-klagomal__lapp p { margin: 0; }

.fb-klagomal figcaption,
.fb-klagomal__kalla {
  margin-top: var(--s-3);
  font-family: var(--f-display);
  font-size: var(--t-xs);
  letter-spacing: var(--spar-display);
  text-transform: uppercase;
  color: var(--papper-djup);                    /* 13.1:1 on valnot */
}

.fb-klagomal__notis { max-width: 14rem; }
.fb-klagomal__notis img { border: 1px solid var(--fb-guld-linje); }

.fb-klagomal__vagg {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-6);
  align-items: flex-start;
  margin-top: var(--s-6);
}

.fb-anka { width: min(100%, 27rem); margin-inline: auto; }

.fb-anka__oval {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1.04;
}

.fb-anka__hal {
  position: absolute;
  inset: 8.5% 9%;
  border-radius: 50% / 46%;
  overflow: hidden;
  background:
    radial-gradient(70% 60% at 50% 30%, var(--tra), var(--valnot-mjuk) 70%, var(--valnot));
  display: grid;
  place-items: end center;
}

.fb-anka__anka {
  width: 56%;
  height: auto;
  align-self: end;
  margin-bottom: 11%;
  filter: drop-shadow(0 10px 16px rgb(0 0 0 / 0.55));
}

.fb-anka__ram {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  pointer-events: none;
}

/* The museum label under the frame. Two lines: the title they would put on a
   plaque, and the joke that only works because the title is straight. */
.fb-anka__platta {
  position: relative;
  margin: var(--s-4) auto 0;
  max-width: 24rem;
  text-align: center;
  padding: var(--s-3) var(--s-4);
  background: linear-gradient(var(--valnot-mjuk), var(--valnot));
  border: 1px solid var(--fb-guld-linje);
  font-family: var(--f-display);
  font-size: var(--t-xs);
  letter-spacing: var(--spar-vid);
  text-transform: uppercase;
  color: var(--guld);                           /* 6.91:1 on valnot */
  display: grid;
  gap: 0.4rem;
}
.fb-anka__titel {
  font-weight: 700;
  color: var(--guld-ljus);                      /* 10.26:1 on valnot */
}

@media (max-width: 60rem) {
  .fb-klagomal__inre { grid-template-columns: 1fr; }
  .fb-klagomal__inre > .fb-anka { order: -1; }
}
/* 6. Degradation ======================================================== */

/* Reduced motion: the poster is the hero and hero-film.js never asks for a single
   byte of video. The clock and the beat rail go with it -- both are readouts
   of a playhead that does not exist here, and a readout of nothing is a lie
   told quietly. The same is true without JavaScript: data-film is only ever
   set by the script, and the rules in section 3 already hide both. */
@media (prefers-reduced-motion: reduce) {
  .fb__film { display: none; }
}

/* 2c. THE MARK COMES DOWN AND LANDS ================================= */

/* Dennis, 2026-08-07: "efter texten fadeas vill jag att loggan studsar ner
   till typ botten mitten och under loggan boka bord och se menyn."

   So the card's WORDS fade and the MARK travels. There is one mark, not two:
   it lives in the hero and is lifted up into the card while the card plays, so
   when the words go it is already on screen and simply comes home. Handing off
   between two elements would cross-fade, and a cross-fade is not the same
   gesture as a thing coming down to land.

   TWO ELEMENTS, ONE JOB EACH, and this is load-bearing rather than tidiness.
   An element has one transform. The mark has to travel AND squash, and the
   travel is a transition while the squash is a keyframe animation -- put both
   on one element and the animation wins while it runs, then the element snaps
   to its base style when the animation is removed instead of transitioning
   from where the animation left it. So: .fb__marke travels, .fb__marke-inre
   squashes, and neither can clobber the other.

   The landing is exact by construction. Home is transform:none -- the position
   the layout already gives it -- so the lift can be an approximate viewport
   distance without the mark ever landing a pixel off. Same principle as the
   bounce above: it fails towards being in the right place. */

.fb__marke {
  --fb-lyft: 0px;
  transform: translateY(var(--fb-lyft));
  z-index: 3;                    /* over the card, which is 2 */
}


/* No card, no journey. Under reduced motion and on a metered connection the
   card never plays, so the mark is simply where it belongs from the first
   frame -- and it must not slide when the viewport is resized either. */
@media (prefers-reduced-motion: reduce) {
  .fb__marke { transition: none; }
}

/* 2d. The hero's bottom stack ======================================== */

/* Centred, because the mark is now the thing the eye lands on and a mark
   ranged left with buttons ranged right is two centres of gravity. The order
   is place, sentence, name, action -- the buttons sit directly under the mark,
   which is what was asked for. */
.fb__inre {
  align-items: center;
  text-align: center;
}
/* THE RESERVE IS THE HEART'S OVERHANG, NOT A ROUND NUMBER. The heart is
   absolutely positioned, so it is out of flow and the mark has to hold room for
   it by hand -- but 0.19 of the mark's width plus --s-4 reserved almost twice
   what the heart uses, and the surplus landed as air between the logo and the
   address. Measured at both viewports: the heart hangs 30 px below the mark's box
   at 1440 (mark 272 wide) and 17 px at 375 (mark 160 wide), so the overhang is
   0.11 of the mark's width and holds across the clamp. 0.13 keeps a small
   clearance without paying for a gap nobody asked for.

   Then the proximity is the right way round. The address is the mark's own line
   -- name, then where the name is -- so it has to sit CLOSER to the logo than to
   the buttons, and it was the opposite: 62 px up against 40 px down, which read
   as a caption on the button row. */
.fb__marke { margin-bottom: calc(0.13 * var(--fb-marke-b)); }
.fb__knappar { justify-content: center; }

/* The seat the mark is lifted into. Same box as the mark, drawing nothing --
   its only job is to make the card lay itself out as though the mark were in
   it, because it is about to be. Sized off the same variable as the mark, so
   the two cannot drift apart. */
.fb__intro-plats {
  display: block;
  width: var(--fb-marke-b);
  aspect-ratio: 181 / 79;
  margin-bottom: calc(0.19 * var(--fb-marke-b));
}

/* The lift is MEASURED, not guessed. The landing may be approximate -- home is
   transform:none and cannot be wrong -- but the lifted position may not: it has
   to sit in the card's composition, and a guessed -30svh put the wordmark
   through "presents" and the heart through the title. The script measures the
   seat against the mark's home and writes the difference; the old guess stays
   as the fallback for the case where the script never runs, which is also the
   case where the card never plays. */
.fb[data-intro="start"] .fb__marke,
.fb[data-intro="visar"] .fb__marke { --fb-lyft: var(--fb-lyft-matt, -30svh); }

/* IT FALLS. Dennis: "när den åker ner vill jag att den studsar lite, så man får
   känslan av att den liksom ramlar ner."

   A transition cannot do this. One cubic-bezier can overshoot its target once
   and come back, which reads as a slide that wobbled -- not as weight. Falling
   is a sequence: accelerate under gravity, hit, throw back up a shorter way,
   hit again, and settle. That needs keyframes, and it needs the easing to
   change direction at every contact: accelerating on the way DOWN, decelerating
   on the way UP. A single ease across the whole thing is what makes bounces
   look like rubber instead of like weight.

   Gated on .fb--fallet, set by the script only when the card actually played.
   With the card skipped the mark is already at home, and a mark that drops in
   from nowhere on a plain page load is a glitch, not an entrance.

   The animation ends exactly at the base style (translateY(0)), so there is
   nothing to fill and nothing to snap back from when it is removed. */
.fb--fallet[data-intro="klar"] .fb__marke {
  animation: fb-fall 1150ms both;
}
@keyframes fb-fall {
  0% {
    transform: translateY(var(--fb-lyft-matt, -30svh));
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.35);   /* gravity */
  }
  46% {
    transform: translateY(0);                                     /* contact */
    animation-timing-function: cubic-bezier(0.1, 0.62, 0.5, 1);   /* thrown up */
  }
  62% {
    transform: translateY(-16%);
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.35);
  }
  76% {
    transform: translateY(0);
    animation-timing-function: cubic-bezier(0.1, 0.62, 0.5, 1);
  }
  86% {
    transform: translateY(-5.5%);
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.35);
  }
  94% {
    transform: translateY(0);
    animation-timing-function: cubic-bezier(0.1, 0.62, 0.5, 1);
  }
  97% { transform: translateY(-1.5%); }
  100% { transform: translateY(0); }
}

/* The squash belongs to the contacts, not to the whole drop. It rides on the
   inner element -- see the note above about one transform per element -- and
   its origin is already the bottom centre, which is what makes a squash read as
   weight landing rather than as a shrink. Same duration as the fall so the two
   cannot drift out of step. */
.fb--fallet[data-intro="klar"] .fb__marke-inre {
  animation: fb-nedslag 1150ms both;
}
@keyframes fb-nedslag {
  0%, 38% { transform: scaleX(1) scaleY(1); }
  46%     { transform: scaleX(1.13) scaleY(0.84); }   /* first contact */
  55%     { transform: scaleX(0.97) scaleY(1.05); }
  76%     { transform: scaleX(1.06) scaleY(0.94); }   /* second contact */
  83%     { transform: scaleX(0.99) scaleY(1.02); }
  94%     { transform: scaleX(1.02) scaleY(0.98); }
  100%    { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .fb--fallet[data-intro="klar"] .fb__marke,
  .fb--fallet[data-intro="klar"] .fb__marke-inre { animation: none; }
}

/* ==========================================================================
   HERO-ALTERNATIV. Tre temperament, en markup.

   Variants are classes on .fb, not three copies of the hero. The markup is
   hero_film.hero() in every case, so a copy change lands in all three and none
   of them can quietly drift away from what the site actually ships.

   A is the default and needs no class: mark-led, everything centred low.
   ========================================================================== */

/* REMSAN. Dennis, 2026-08-10: no colour block. Measured over all 100 sampled
   frames in an 8x5 grid, the darkest cell in the whole film still peaks at 139
   of 255, and cream type needs the ground at 111 or below for 4.5:1. Not one
   cell in the frame passes, not even the 3:1 bar for large elements. So there
   is no place on this film where cream type survives unaided, and the choice is
   between putting the type somewhere else or accepting that it fails.

   This is the somewhere else. The film keeps its whole frame with nothing on it;
   the address and the buttons drop into a row UNDER it, on the ground the .fb
   section already paints. The contrast is the palette's, unchanged, because the
   ground is the same walnut it always was -- it just stopped lying on top of the
   picture.

   The drawn mark leaves the hero here, because the header carries it 60 px up
   and repeating it was never an argument. The h1 stays: only .fb__marke-inre
   goes, so the visually hidden "Moldau" is still the page's one heading. Hiding
   the h1 itself would take the name out of the outline with it. */
.fb--remsa { grid-template-rows: 1fr auto; }
.fb--remsa .fb__duk { grid-area: 1 / 1; }
.fb--remsa .fb__inre {
  grid-area: 2 / 1;
  align-self: stretch;
  width: min(100% - 2 * var(--s-4), var(--matt));
  max-width: none;
  margin: 0 auto;
  padding: var(--s-4) 0;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3) var(--s-5);
}
.fb--remsa .fb__inre::before { display: none; }
/* Out of the flex flow entirely, not merely emptied. An h1 with the drawing
   hidden is still a flex item, and a zero-width item in a space-between row
   claims a slot -- it pushed the address off the left edge and into the middle,
   which looked like a centring decision nobody made. Hidden the standard way,
   so the name is still there for a screen reader and for search. */
.fb--remsa .fb__marke {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
.fb--remsa .fb__marke-inre { display: none; }
.fb--remsa .fb__ogonbryn { margin: 0; }
.fb--remsa .fb__knappar { width: auto; }

/* SKUGGAN. The same note answered the other way: no block anywhere, the type
   straight onto the film with nothing behind it but a halo on the glyphs.

   Read the measurement above before choosing this. A shadow does not change the
   ratio between the letter and what is behind it -- over the fryer oil that is
   cream on near-white, about 1.1:1, and the halo is the only thing separating
   them. It reads well in the dark half of the film and the wordmark thins out to
   nothing when the pan flares. That is not a bug in these values, it is the
   trade: this version is the one that covers no film and cannot be certified.
   The card is deliberately shown over f-077, the brightest frame in the whole
   42 seconds, because a still of a dark moment would sell it dishonestly. */
.fb--skugga .fb__inre::before { display: none; }
.fb--skugga .fb__inre { padding: var(--s-5) var(--s-6); }
.fb--skugga .fb__marke-bild {
  filter:
    drop-shadow(0 1px 2px rgb(43 26 14 / 0.95))
    drop-shadow(0 0 10px rgb(43 26 14 / 0.85))
    drop-shadow(0 0 28px rgb(43 26 14 / 0.6));
}
.fb--skugga .fb__ogonbryn,
.fb--skugga .knapp--linje {
  text-shadow:
    0 1px 2px rgb(43 26 14 / 0.95),
    0 0 9px rgb(43 26 14 / 0.9),
    0 0 22px rgb(43 26 14 / 0.7);
}
.fb--skugga .knapp--linje {
  border-color: rgb(245 239 228 / 0.85);
  box-shadow: 0 2px 14px rgb(43 26 14 / 0.55);
}

/* B — VÄNSTERSTÄLLD. The editorial reading. Type ranged left against the frame
   edge the way a magazine sets a cover line, mark above it at reading size
   rather than as the hero's centrepiece. Calmer, and it leaves the whole right
   half of the film uncovered -- which matters here, because the right half is
   where the hands are. */
.fb--vanster .fb__inre { align-items: start; text-align: left; }
.fb--vanster .fb__marke { width: calc(0.62 * var(--fb-marke-b)); }
.fb--vanster .fb__knappar { justify-content: flex-start; }

/* C — DELAD. The mark sits high, in the frame's optical centre, and the page
   furniture is pushed out to the bottom corners: address and year left, buttons
   right, the sentence under the mark. The most cinematic of the three because
   the middle of the picture stays empty -- a title over a shot, with the
   controls at the edges where they do not compete with it. */
.fb--delad .fb__inre {
  display: grid;
  grid-template-columns: 1fr auto;
  /* Two rows: the mark across the top, the address in one bottom corner and
     the button in the other. The tagline and the year had rows of their own
     until they were cut on 2026-08-07, and an empty named area is a hole the
     row gap still pays for. */
  grid-template-areas:
    "marke   marke"
    "adress  knappar";
  align-items: end;
  gap: var(--s-2) var(--s-6);
  padding-block-start: 30svh;
}
.fb--delad .fb__marke   { grid-area: marke; justify-self: center; }
.fb--delad .fb__ogonbryn { grid-area: adress; align-self: end; margin: 0; text-align: left; }
.fb--delad .fb__knappar { grid-area: knappar; align-self: end; justify-content: flex-end; margin: 0; }

@media (max-width: 46rem) {
  /* Both alternatives collapse to the centred stack on a phone. A split row is
     two columns of four words each at 390 px, and a left-ranged block under a
     centred mark reads as a mistake rather than as a choice. */
  .fb--vanster .fb__inre,
  .fb--delad .fb__inre {
    display: flex; flex-direction: column;
    align-items: center; text-align: center;
    padding-block-start: 7rem;
  }
  .fb--vanster .fb__marke { width: var(--fb-marke-b); }
  .fb--vanster .fb__knappar, .fb--delad .fb__knappar { justify-content: center; }
  .fb--delad .fb__ogonbryn { text-align: center; margin: var(--s-2) 0 var(--s-5); }
}

/* ==========================================================================
   PROJEKTIONEN. Three ways to make the film read as old, as classes on .fb.

   None of them is applied by default -- the markup ships the layer, the layer
   draws nothing until a class asks it to.

   The point worth stating: a colour grade alone reads as a filter, not as film.
   What sells age is MOVEMENT -- grain that lives, exposure that breathes, the
   frame wandering a pixel in the gate. So the three are not three strengths of
   the same idea. A is colour, B is texture, C is motion.

   Everything animated here is transform or opacity, so nothing relayouts and
   nothing repaints per frame. Under prefers-reduced-motion all three fall back
   to their still grade, which is the honest degradation: an old photograph
   instead of an old film.

   The type is untouched by all of it. The text block carries its own veil as a
   background and sits in a later grid layer, so the measured 9.21:1 stands
   whatever the film does underneath.
   ========================================================================== */

.fb__gejd {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;                          /* nothing, until a variant asks */
}

/* Grain, as one tiled turbulence tile on an oversized layer that is nudged by
   whole tiles. Stepping a transform is free; animating background-position
   would repaint the whole surface every frame. */
@keyframes fb-korn {
  0%   { transform: translate3d(0, 0, 0); }
  20%  { transform: translate3d(-6%, -3%, 0); }
  40%  { transform: translate3d(-3%, -6%, 0); }
  60%  { transform: translate3d(-6%, -1%, 0); }
  80%  { transform: translate3d(-1%, -4%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* The gate: the whole picture wanders a fraction of a pixel-percent, the way
   a print does when the sprockets are worn. Tiny on purpose -- at anything
   visible it reads as a broken page rather than as a projector. */
@keyframes fb-vandring {
  0%   { transform: translate3d(0, 0, 0) scale(1.012); }
  25%  { transform: translate3d(0.16%, -0.12%, 0) scale(1.012); }
  50%  { transform: translate3d(-0.1%, 0.14%, 0) scale(1.012); }
  75%  { transform: translate3d(0.12%, 0.08%, 0) scale(1.012); }
  100% { transform: translate3d(0, 0, 0) scale(1.012); }
}

/* Exposure breathing. Uneven on purpose: a regular pulse reads as a fade. */
@keyframes fb-flimmer {
  0%, 100% { opacity: 0.10; }
  17%      { opacity: 0.02; }
  31%      { opacity: 0.14; }
  46%      { opacity: 0.05; }
  63%      { opacity: 0.12; }
  81%      { opacity: 0.03; }
}

/* --- A — NITRATET. Colour only. A warm print kept in a dry cupboard: amber
   shift, blacks lifted the way a positive never quite reaches black, and a
   vignette from the lens. No grain, no flicker. The most restrained, and the
   only one that costs nothing at all to run. */
.fb--film-a .fb__ruta,
.fb--film-a .fb__film {
  filter: sepia(0.34) saturate(0.72) contrast(0.92) brightness(1.04)
          hue-rotate(-8deg);
}
.fb--film-a .fb__gejd {
  opacity: 1;
  background:
    radial-gradient(120% 100% at 50% 45%,
      rgb(43 26 14 / 0) 42%, rgb(43 26 14 / 0.34) 78%, rgb(43 26 14 / 0.58) 100%),
    linear-gradient(to bottom, rgb(201 162 39 / 0.10), rgb(122 74 34 / 0.10));
}

/* --- B — ARKIVET. Texture. Most of the colour goes, the grain arrives, and
   the print gets the density of newsreel stock. This is the one that reads as
   "found footage" rather than "old-looking". */
.fb--film-b .fb__ruta,
.fb--film-b .fb__film {
  filter: sepia(0.62) saturate(0.45) contrast(1.14) brightness(0.96);
}
.fb--film-b .fb__gejd {
  opacity: 1;
  /* feTurbulence rendered once into a 160px tile. An SVG data URI rather than
     a PNG so it weighs 300 bytes and stays sharp at any density. */
  background-image:
    radial-gradient(120% 100% at 50% 45%,
      rgb(20 12 6 / 0) 38%, rgb(20 12 6 / 0.44) 76%, rgb(20 12 6 / 0.72) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 100% 100%, 160px 160px;
  background-repeat: no-repeat, repeat;
  mix-blend-mode: multiply;
}
.fb--film-b .fb__gejd::after {
  content: "";
  position: absolute;
  inset: -8%;
  background-image: inherit;
  background-size: 160px 160px;
  background-repeat: repeat;
  opacity: 0.5;
  animation: fb-korn 900ms steps(5) infinite;
}

/* --- C — PROJEKTORN. Motion. The grade is the lightest of the three on
   purpose: here the age is carried by the picture breathing and wandering, and
   a heavy grade on top of that is two effects fighting. Look away from it and
   you still feel it; that is the test it is built to pass. */
.fb--film-c .fb__ruta,
.fb--film-c .fb__film {
  filter: sepia(0.20) saturate(0.86) contrast(0.96) brightness(1.02);
  animation: fb-vandring 5.5s ease-in-out infinite;
  /* Scaled 1.2 % inside the frame so the wander never exposes an edge. */
  transform: scale(1.012);
}
.fb--film-c .fb__gejd {
  opacity: 1;
  background:
    radial-gradient(120% 100% at 50% 45%,
      rgb(43 26 14 / 0) 44%, rgb(43 26 14 / 0.30) 80%, rgb(43 26 14 / 0.52) 100%);
}
.fb--film-c .fb__gejd::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(255 246 224);
  mix-blend-mode: overlay;
  animation: fb-flimmer 2.3s steps(1) infinite;
}

@media (prefers-reduced-motion: reduce) {
  /* The grade stays, the projector stops. An old photograph, not an old film. */
  .fb--film-b .fb__gejd::after,
  .fb--film-c .fb__gejd::after,
  .fb--film-c .fb__ruta,
  .fb--film-c .fb__film { animation: none; }
  .fb--film-c .fb__gejd::after { opacity: 0.06; }
}
