/* ==========================================================================
   Gustave Kamanzi — Arkitekt + BIM Manager
   1:1 structural replica of the clementgrellier.fr design system.
   Plain CSS, no build step. rem base is 10px (html{font-size:10px}).
   ==========================================================================
   Sections:
     1. Fonts
     2. Reset
     3. Tokens & base
     4. A11y helpers
     5. Layout primitives
     6. Media placeholders (.ph)
     7. Page transition (.transition + enter state)
     8. Pill nav
     8b. Language switcher (top right)
     9. CTA pill + underline link
    10. Custom cursor
    11. Home (intro)
    11b. Home hero reel (.reel)
    12. About
    13. Overview (3D vertical carousel)
   ========================================================================== */

/* 1. Fonts ------------------------------------------------------------------
   Licensed Amiamie files live in ../fonts/ (generated from the .otf originals
   in amiamie-font-family/ — see BUILD-NOTES §Fonts for the conversion command):
     - Amiamie-Regular.woff2 / .woff  (family "Amiamie",      weight 400)
     - Amiamie-Light.woff2   / .woff  (family "AmiamieLight", weight 300)
   Both faces are preloaded from every page's <head>. The fallback stack below
   (Helvetica Neue / Arial) still renders the full site if the files are removed.
   Note: Amiamie has no arrow glyphs, so "→" resolves from the fallback — same
   behaviour as the reference site. */
@font-face {
  font-family: "AmiamieLight";
  src: url("../fonts/Amiamie-Light.woff2") format("woff2"),
       url("../fonts/Amiamie-Light.woff") format("woff");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Amiamie";
  src: url("../fonts/Amiamie-Regular.woff2") format("woff2"),
       url("../fonts/Amiamie-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* Metric-matched stand-in for the swap period (font-display:swap paints the
   fallback until Amiamie arrives). Helvetica Neue and Arial set the same
   uppercase string ~6% wider than Amiamie — enough to push a case title such
   as NORMORIA OPERA out of its 31.5vw box and break it onto a second line
   (Chrome with a Norwegian dictionary hyphenates it: "NORMORIA OPE-RA").
   size-adjust:93% is the measured Amiamie/fallback advance-width ratio,
   averaged over all 14 case titles; 13 of them hold their line count across
   the swap. TROMSØ LUFTHAVN GATE 25 is the exception — it sets 2 lines in the
   stand-in and 3 in Amiamie. Only 97% matches all 14, but that leaves NORMORIA
   OPERA just 1.5% clear of wrapping, so the margin on the title this fix is
   for wins over one line of shift on another page during the swap window.
   Local-only src: if none of these resolve the face is skipped and the plain
   stack below applies. */
@font-face {
  font-family: "Amiamie Fallback";
  src: local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"),
       local("Arial Bold"), local("Arial-BoldMT"),
       local("Helvetica Neue"), local("Arial");
  font-weight: 700;
  font-style: normal;
  size-adjust: 93%;
}

/* 2. Reset (verbatim from original) */
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{font-size:100%;font:inherit;vertical-align:baseline;border:0;margin:0;padding:0}
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}
body{line-height:1}
ol,ul{list-style:none}
blockquote,q{quotes:none}
blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}
table{border-collapse:collapse;border-spacing:0}
*{box-sizing:border-box}

/* 3. Tokens & base */
:root{
  --smooth:cubic-bezier(.15,.9,.34,.95);
  --custom:cubic-bezier(.76,0,.24,1);
}
/* -webkit-text-size-adjust:100% stops iOS Safari from auto-inflating body
   copy in portrait: the rem base here is 10px and nearly all display type is
   vw-based, so an engine-applied boost scales paragraphs but not headings and
   the type hierarchy inverts. */
html{font-size:10px;background-color:#fff;overflow-x:hidden;-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{
  color:#000;
  font-family:Amiamie,"Helvetica Neue",Arial,sans-serif;
  font-size:1.6rem;
  line-height:1.5;
  text-rendering:optimizeLegibility;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
}
a,a:hover{text-decoration:none}
a{color:#000}
img{max-width:100%}
::selection{color:#fff;text-shadow:none;background:#000}
.page{background-color:#fff;position:relative}
.full{object-fit:cover;object-position:center;width:100%;height:100%;display:block}

/* 4. A11y helpers */
a:focus-visible,button:focus-visible,[tabindex]:focus-visible{outline-offset:3px;border-radius:2px;outline:2px solid #000}
.skip-link{z-index:9999;color:#fff;background:#000;border-radius:0 0 .4rem .4rem;padding:.8rem 1.6rem;font-size:1.4rem;text-decoration:none;transition:top .2s;position:absolute;top:-100%;left:1rem}
.skip-link:focus{top:0}
.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}
@media (prefers-reduced-motion:reduce){
  *,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}
}

/* 5. Layout primitives */
.container{padding:0 2vw}
@media screen and (max-width:991px){.container{padding:0 4vw}}
.wrapper{justify-content:space-between;display:flex}
@media screen and (max-width:991px){.wrapper{flex-wrap:wrap}}
.wrapper .column{width:50%}
@media screen and (max-width:991px){.wrapper .column{width:100%}}

/* 6. Media placeholders (.ph) ------------------------------------------------
   Every image/video slot is a flat block — near-black #0a0a0a or accent blue
   #1f3bff — with a small lowercase label telling the editor what to swap in.
   Use .ph--blue for the blue variant and .ph--video for video slots.
   Give the box its shape with an inline aspect-ratio or a fixed height. */
.ph{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:#0a0a0a;overflow:hidden}
.ph--blue{background-color:#1f3bff}
.ph__label{padding:1rem;font-size:1.2rem;line-height:1.4;color:rgba(255,255,255,.6);text-transform:lowercase;text-align:center}
.ph--video .ph__label:before{content:"";display:block;width:0;height:0;margin:0 auto 1.2rem;border-left:1.4rem solid rgba(255,255,255,.6);border-top:.9rem solid transparent;border-bottom:.9rem solid transparent}

/* 6b. Real media (.media) ----------------------------------------------------
   Slots that hold actual photography/video instead of a .ph block. The box is
   deliberately identical to .ph (relative, overflow:hidden, fills its parent,
   near-black while the file loads) so every animation written against .ph —
   the work-grid hover scale, the trio squeeze, the hero clip-path — applies
   unchanged. The <img>/<video> inside carries .full (object-fit:cover).
   .media--contain is for drawings, which must be seen whole, not cropped; it
   keeps the surrounding surface rather than painting its own letterbox, so a
   drawing on the white page never picks up black bars down its sides. */
.media{position:relative;width:100%;height:100%;background-color:#0a0a0a;overflow:hidden}
.media--contain{background-color:transparent}
.media--contain .full{object-fit:contain}

/* 7. Page transition ---------------------------------------------------------
   .transition is the black overlay inside each .page. js/main.js plays:
   enter (1s): page-wrapper slides from x:100vw over the black overlay, overlay
   fades out. leave (0.6s): page-wrapper slides to x:-70vw with blur(10px)
   while the overlay fades back in — then the browser navigates (no framework).
   The exit is deliberately shorter than the arrival: leaving is a decision
   already made, arriving is the part worth watching. Links are prefetched on
   hover, so the navigation itself costs ~no time between the two halves. */
.transition{opacity:0;visibility:hidden;z-index:10;background-color:#000;width:100%;height:100%;position:absolute;top:0;left:0}
/* Pre-animation state, added by an inline script in <head> to avoid FOUC.
   Removed by js/main.js once the enter animation completes (and by a 3s
   safety timeout in the inline script if GSAP fails to load). */
html.is-entering{overflow:hidden}
html.is-entering .page-wrapper{position:relative;z-index:11;transform:translateX(100vw)}
html.is-entering .transition{opacity:1;visibility:visible}
@media (prefers-reduced-motion:reduce){
  html.is-entering{overflow:auto}
  html.is-entering .page-wrapper{transform:none}
  html.is-entering .transition{opacity:0;visibility:hidden}
}

/* 8. Pill nav (fixed bottom left) --------------------------------------------
   The black active indicator is positioned via --nav-x / --nav-w custom
   properties, measured from the .active item by js/main.js (the original
   hard-coded pixel widths; ours are computed so they fit the Norwegian
   labels and the fallback font). */
/* Anchored in the bottom-left corner on the same 2.4rem inset the language
   switcher uses in the top-right, so the two controls frame the page from
   opposite corners. Deliberately `left`, not the original's
   left:0/right:0/margin:auto centring: WebKit does not resolve `margin:auto`
   for an over-constrained *table* box that is also positioned, so that pair
   rendered left in Safari and centred everywhere else — the same pill in two
   different places depending on the browser. Stating the corner outright
   makes every engine agree. Below 991px it still centres (see §8 mobile),
   where a corner-pinned pill would crowd the edge of a phone screen. */
.nav{--nav-x:0px;--nav-w:0px;z-index:10;transition:all .8s var(--custom);display:table;position:fixed;bottom:2.4rem;left:2.4rem;transform:translateY(10rem)}
@media screen and (max-width:991px){.nav{left:50%;transform:translate(-50%,10rem)}}
.nav__menu{align-items:center;height:4.8rem;display:flex;position:relative}
.nav__menu:before{content:"";width:0;height:100%;transition:width .8s var(--custom);background-color:#ededed;border-radius:2.4rem;margin:auto;display:block;position:absolute;top:0;left:0;right:0}
.nav__menu:after{content:"";height:100%;transition:all .6s var(--custom);background-color:#000;border-radius:2.4rem;width:var(--nav-w);left:var(--nav-x);display:block;position:absolute;top:0}
.nav__menu__item{z-index:2;opacity:0;visibility:hidden;height:100%;transition:opacity .6s var(--smooth),visibility .6s var(--smooth);position:relative}
.nav__menu__item__link{color:#000;align-items:center;height:100%;padding:0 2.4rem;display:flex;position:relative;overflow:hidden;white-space:nowrap}
.nav__menu__item__link.active{color:#fff}
.nav__menu__item__link span{transition:all .4s var(--custom);display:block}
.nav__menu__item__link .hover{filter:blur(5px);position:absolute;transform:translateY(200%)scale(1.2)}
@media screen and (min-width:992px){
  .nav__menu__item__link:hover .default{filter:blur(5px);transform:translateY(-200%)scale(.8)}
  .nav__menu__item__link:hover .hover{filter:blur(0);transform:translate(0,0)scale(1)}
}
.nav.show{transform:translateY(0)}
@media screen and (max-width:991px){.nav.show{transform:translate(-50%,0)}}
.nav.show .nav__menu:before{transition:width .8s var(--custom);width:100%}
.nav.show .nav__menu__item{opacity:1;visibility:visible;transition:opacity .6s var(--smooth) .8s,visibility .6s var(--smooth) .8s}

/* 8b. Language switcher (fixed top right) -------------------------------------
   Same vocabulary as the bottom pill nav — #ededed track, black indicator,
   blur-flip hover — at ~2/3 scale, and it rises from the top as the nav rises
   from the bottom (js/main.js adds .show to both at the same moment).
   Two items of equal fixed width, so the indicator is a plain 50% slide and
   needs no JS measuring: [data-active] on .lang is set by js/i18n.js. */
.lang{z-index:10;transition:all .8s var(--custom);position:fixed;top:2.4rem;right:2.4rem;transform:translateY(-10rem)}
.lang__menu{align-items:center;height:3.2rem;display:flex;position:relative}
.lang__menu:before{content:"";width:0;height:100%;transition:width .8s var(--custom);background-color:#ededed;border-radius:1.6rem;display:block;position:absolute;top:0;right:0}
.lang__menu:after{content:"";width:50%;height:100%;transition:all .6s var(--custom);background-color:#000;border-radius:1.6rem;display:block;position:absolute;top:0;left:0;opacity:0}
.lang[data-active="en"] .lang__menu:after{left:50%}
.lang.show .lang__menu:after{opacity:1}
.lang__menu__item{z-index:2;opacity:0;visibility:hidden;height:100%;transition:opacity .6s var(--smooth),visibility .6s var(--smooth);position:relative}
.lang__item__link{color:#000;font-size:1.3rem;letter-spacing:.02em;justify-content:center;align-items:center;width:4.4rem;height:100%;display:flex;position:relative;overflow:hidden;white-space:nowrap}
.lang__item__link.active{color:#fff}
.lang__item__link span{transition:all .4s var(--custom);display:block}
.lang__item__link .hover{filter:blur(5px);position:absolute;transform:translateY(200%)scale(1.2)}
@media screen and (min-width:992px){
  .lang__item__link:hover .default{filter:blur(5px);transform:translateY(-200%)scale(.8)}
  .lang__item__link:hover .hover{filter:blur(0);transform:translate(0,0)scale(1)}
}
.lang.show{transform:translateY(0)}
.lang.show .lang__menu:before{transition:width .8s var(--custom);width:100%}
.lang.show .lang__menu__item{opacity:1;visibility:visible;transition:opacity .6s var(--smooth) .8s,visibility .6s var(--smooth) .8s}
@media screen and (max-width:991px){.lang{top:1.6rem;right:1.6rem}}

/* 8c. Instant nav state (arriving from elsewhere on the site) -----------------
   The original site never unmounts its <nav>: it lives outside the swapped
   container, so across a transition the pill only glides its black indicator
   to the new item. We are multi-page, so the nav really does unmount — these
   rules put it straight back in its final pose on the arriving page's first
   frame, with every transition suppressed so nothing replays or slides.
   js/main.js drops .nav-instant after one painted frame, at which point the
   normal .show rules take over at identical values and hover/resize animate
   again. The outgoing page does the indicator glide (see main.js handOverNav),
   so the pill appears to move once, continuously, across the navigation. */
html.nav-instant .nav,
html.nav-instant .lang,
html.nav-instant .nav__menu:before,
html.nav-instant .lang__menu:before,
html.nav-instant .nav__menu:after,
html.nav-instant .lang__menu:after,
html.nav-instant .nav__menu__item,
html.nav-instant .lang__menu__item{transition:none}
html.nav-instant .nav{transform:translateY(0)}
@media screen and (max-width:991px){html.nav-instant .nav{transform:translate(-50%,0)}}
html.nav-instant .lang{transform:translateY(0)}
html.nav-instant .nav__menu:before,
html.nav-instant .lang__menu:before{width:100%}
html.nav-instant .lang__menu:after{opacity:1}
html.nav-instant .nav__menu__item,
html.nav-instant .lang__menu__item{opacity:1;visibility:visible}

/* 9. CTA pill + underline link */
.cta{color:#000;height:4.8rem;transition:background-color .4s var(--custom);border:1px solid #000;border-radius:2.4rem;align-items:center;padding:0 2.4rem;display:inline-flex;position:relative;overflow:hidden}
.cta span{transition:all .4s var(--custom);display:block}
.cta .hover{color:#fff;filter:blur(5px);position:absolute;transform:translateY(150%)scale(1.2)}
@media screen and (min-width:992px){
  .cta:hover{background-color:#000}
  .cta:hover .default{filter:blur(5px);transform:translateY(-150%)scale(.8)}
  .cta:hover .hover{filter:blur(0);transform:translate(0,0)scale(1)}
}
.link-overview{color:#000;font-size:1.4rem;position:relative}
@media screen and (min-width:992px){
  .link-overview:after{content:"";width:100%;height:1px;transition:transform .4s var(--smooth);transform-origin:100%;background-color:#000;display:block;position:absolute;bottom:0;left:0;transform:scaleX(0) translateZ(0)}
  .link-overview:hover:after{transform-origin:0;transform:scaleX(1) translateZ(0)}
}

/* 10. Custom cursor (desktop >1024px only, driven by js/main.js) */
.cursor{z-index:500;pointer-events:none;will-change:transform;width:2rem;height:2rem;transition:transform .9s var(--smooth),opacity .3s var(--smooth);position:fixed;top:-1rem;left:-1rem;transform:translate(-100px,-100px)}
.cursor:after{content:"";opacity:.4;width:100%;height:100%;transition:all .4s var(--smooth);background-color:#e4e4e4;border-radius:50%;display:block;position:absolute;top:0;left:0}
.cursor.hover:after{opacity:.8;background-color:#000;transform:scale(.8)}
@media screen and (max-width:1024px){.cursor{display:none}}

/* 11. Home — two-panel intro -------------------------------------------------
   Phase 1: black panel with three white 10vw AmiamieLight lines fades in.
   Phase 2: .page.animate (added by js/home.js) slides the 200%-wide wrap
   -50% after 1.2s, revealing the white final layout. */
.intro{color:#000;background-color:#000;width:100%;height:100vh;overflow:hidden}
.intro__wrap{width:200%;height:100%;display:flex}
.intro__left{width:50%;height:100%;position:relative}
.intro__right{background-color:#fff;width:50%;height:100%;position:relative}
.intro__right h1{letter-spacing:-.05em;padding:0 1.2vw;font-family:Amiamie,"Helvetica Neue",Arial,sans-serif;font-size:11.1vw;line-height:1}
/* "GUSTAVE KAMANZI" splits into two stacked lines (7+7 chars, per content map) */
.intro__right h1 .word{display:block}
@media screen and (max-width:991px){.intro__right h1{padding:0 3vw;font-size:10.7vw}}
.intro__right h2{margin-top:2.4rem;font-size:2.4rem;line-height:1}
.intro__video{width:1080px;max-width:100%;margin-top:-2vh}
@media screen and (max-width:1200px){.intro__video{width:100%;margin-top:0}}
/* The two columns are the same box (flex stretch), but their contents are not:
   the right one is pushed down by .intro__content's 25vh and ends on the CTA
   row, while the left one (reel + tagline) is content-height and ends ~141px
   short at 1440x1108, leaving the reel high enough to clip the h1's descenders.
   flex-end drops the left column onto the same baseline as the CTA row, which
   both closes that gap and lifts the reel clear of the name. Desktop only —
   below 992px .wrapper wraps to one column per line, where cross-axis
   alignment has nothing left to align. */
@media screen and (min-width:992px){
  .intro__right .wrapper{align-items:flex-end}
}
.intro__content{padding-top:25vh;padding-left:30%}
.intro__content p{text-align:left;width:34rem;font-family:AmiamieLight,"Helvetica Neue",Arial,sans-serif;font-weight:300}
.intro__content__links{align-items:center;margin-top:2.4rem;display:flex;flex-wrap:wrap;gap:1.2rem 0}
.intro__content .link-overview{color:#000;margin-left:2.4rem;font-size:1.4rem;position:relative}
/* Mobile. The reference indents this block 14% and holds the paragraph at 90%,
   still justified, which lands on a 278px measure at 390px. That does not
   transfer, and the reason is arithmetic rather than taste. The nav pill is
   fixed over the bottom of the screen at y592-640 and is opaque (#ededed), so
   anything under it is not dimmed, it is gone. On a 390x664 screen the second
   column starts at y385, which leaves 207px of clear space above the pill.

     reference, 4 lines of English at 278px   90px   -> ends y525, clears
     ours, indented, 9 lines at 278px        203px   -> ends y638, 46px buried
     ours, flush, 7 lines at 359px           158px   -> ends y593, clears

   The indent costs two lines of measure and buries the tail of the paragraph;
   holding the reference's 5rem padding-top on top of it is impossible, since
   385 + 50 + 203 overshoots the pill on its own. So the block stays flush and
   ragged-right, which is also what an earlier pass concluded from the rivers
   that justify opens at this measure.

   The CTA row is deliberately left in flow rather than nudged clear of the
   pill. Measured, the space the row would have to claw back to sit above it:

     320x568  208px      390x664   76px      480x800  fits
     360x640  126px      414x715   41px

   208px at 320x568 is more than the whole hero video (149px), so there is no
   arrangement of this copy that clears the pill on a small phone; and a rem
   offset that pushes the row below the fold only works at one viewport height,
   since the fold moves and the offset does not (it fixed 390x664 and broke
   414x715). So the row is scrolled to, which is exactly what the reference
   does with its own work tiles — those sit under the pill at y432-627 on both
   sites, to the pixel. The reference's intro clears only because its bio is
   four short English lines against our six-to-eight Norwegian ones.

   The links keep flex-wrap and a gap instead of the reference's nowrap +
   margin-left because "Se prosjektene mine" is wider than "See my projects";
   on one line the 2.4rem gap renders identically. */
@media screen and (max-width:991px){
  .intro__content{padding-top:5rem;padding-left:0}
  .intro__content p{width:100%;font-size:1.5rem;text-align:left}
  .intro__content__links{gap:1.6rem 2.4rem}
  .intro__content .link-overview{margin-left:0}
}
.intro__title{letter-spacing:-.1em;opacity:0;color:#fff;transition:letter-spacing .8s var(--smooth),opacity .8s var(--smooth);will-change:opacity,letter-spacing;backface-visibility:hidden;font-family:AmiamieLight,"Helvetica Neue",Arial,sans-serif;font-weight:300;font-size:10vw;line-height:1;position:absolute;top:0;left:0;transform:translate(0,0)}
.intro__title:nth-child(2){top:40%;left:40%}
.intro__title:nth-child(3){inset:auto 1vw 0 auto}
.page.animate .intro__wrap{transition:transform 1s var(--custom) 1.2s;transform:translate(-50%)}
.page.animate .intro__title{opacity:1;letter-spacing:-.06em}
@media screen and (max-width:991px){.page.animate .intro__title{letter-spacing:-.02em}}
.page.animate .intro__title:nth-child(2){transition-delay:.4s}
.page.animate .intro__title:nth-child(3){transition-delay:.6s}
/* Mobile: content is taller than one viewport, so let the page grow and
   scroll instead of clipping (desktop stays exactly 100vh, no scroll). */
@media screen and (max-width:991px){
  .intro{height:auto;min-height:100vh}
  .intro__right{padding-bottom:12rem}
}

/* 11b. Home — hero reel (.reel) ----------------------------------------------
   Sits in the box the hero video placeholder used to fill: .intro__video is
   untouched, and .reel carries the placeholder's own 1420/720 so the hero
   holds its height before a single image has decoded and nothing reflows as
   they arrive.
   One slide is visible at a time (overflow:hidden). The track is a nowrap flex
   row whose items are each 100% of the REEL's width — the track itself stays
   100% wide, so translateX(-100% * i) from js/home.js steps exactly one slide
   whatever the slide count, and no width has to be computed anywhere.
   The step is a transform transition, so it is composited: 1s var(--custom),
   the same ease and duration the page transition uses, which is what makes the
   reel read as part of the same site rather than a widget dropped into it.
   .is-snapping is the seamless wrap. home.js appends a copy of the first slide,
   so the step off the last image lands on a picture identical to the first;
   with transitions off it then sets the track back to index 0 — the transform
   changes, the pixels do not, and the loop never shows a rewind.
   Without JS the track sits at translateX(0) and the first slide is simply the
   hero image. Under prefers-reduced-motion the global rule in §4 already
   flattens the transition and home.js never advances, so the reel is that one
   image and nothing else — hence the hidden button, which would otherwise be a
   control that does nothing. */
.reel{width:100%;aspect-ratio:1420/720;background-color:#0a0a0a;position:relative;overflow:hidden}
.reel__track{width:100%;height:100%;transition:transform 1s var(--custom);will-change:transform;display:flex}
.reel__track.is-snapping{transition:none}
.reel__slide{flex:0 0 100%;height:100%}
/* Full-box hit area, the same shape as .work .project__link: click or Enter
   advances, focus pauses. Transparent — the photographs are the interface, and
   the custom cursor already picks it up as a <button>. */
.reel__advance{z-index:2;appearance:none;cursor:pointer;background:none;border:0;width:100%;height:100%;padding:0;position:absolute;top:0;left:0}
@media (prefers-reduced-motion:reduce){.reel__advance{display:none}}

/* 12. About */
@media screen and (max-width:991px){.about{padding-top:10vh}}
.about h1{letter-spacing:-.02em;font-size:10vw}
@media screen and (max-width:991px){.about h1{font-size:20vw}}
.about__content{transition:opacity 1s var(--smooth)}
html.js .about__content{opacity:0}
html.js .about__content.is-visible{opacity:1}
.about__content p{width:40rem;font-size:1.8rem}
/* Second paragraph added when the intro was rewritten; desktop had no rule for
   the break, so the two ran together. Mobile already spaces every paragraph
   with margin-bottom, hence the reset — otherwise the two would stack. */
.about__content p+p{margin-top:1.8rem}
@media screen and (max-width:991px){
  .about__content p{width:100%;margin-bottom:5vh}
  .about__content p+p{margin-top:0}
}
.about__content__links{align-items:center;gap:2.4rem;margin-top:4.8rem;display:flex;flex-wrap:wrap}
html.js .about__content__links{opacity:0}
@media screen and (max-width:991px){.about__content__links{margin-bottom:8rem}}
html.js .about__content__links.show{opacity:1;transition:opacity 1s var(--smooth)}
.about .portrait{height:100vh;padding:2vw 0 6vw}
@media screen and (max-width:991px){.about .portrait{height:50vh;padding:0}}
.about .what h2{font-size:6vw}
@media screen and (max-width:991px){
  .about .what{padding-top:20vh}
  .about .what h2{font-size:12vw}
}
.about .what p{width:60rem;font-size:3rem}
/* The section ran a single paragraph until "Hva jeg gjør" was expanded to four,
   so nothing had ever set the space between them: desktop had no rule at all
   and would have run them together, and mobile's 6rem margin-top is the gap
   under the h2, which is too wide to double as a paragraph break at 1.8rem.
   Both mirror .case__text, where the break matches the type size. */
.about .what p+p{margin-top:3rem}
@media screen and (max-width:991px){
  .about .what p{width:100%;margin-top:6rem;font-size:1.8rem}
  .about .what p+p{margin-top:2.4rem}
}
/* Recognition ("verv") + education lists — rows with black scaleY fill hover */
.about .awards{padding:20vh 0}
.about .education{padding-bottom:20vh}
@media screen and (max-width:991px){
  .about .awards{padding:10vh 0}
  .about .education{padding-bottom:10vh}
}
.about .awards>p{margin-top:4rem}
.about .awards h2,.about .education h2{font-size:6vw}
@media screen and (max-width:991px){
  .about .awards h2,.about .education h2{margin-bottom:6rem;font-size:12vw}
}
.about .education__list{margin-top:4.8rem}
.awards__item{border-bottom:1px solid #dedede;justify-content:space-between;align-items:center;padding:4rem 0;font-size:1.8rem;display:flex;position:relative}
.awards__item:before{content:"";transform-origin:bottom;width:100%;height:100%;transition:transform .6s var(--smooth);background-color:#000;display:block;position:absolute;top:0;left:0;transform:scaleY(0)}
.awards__item a{z-index:2;width:100%;height:100%;display:block;position:absolute;top:0;left:0}
.awards__item span{z-index:1;width:33%;transition:all .6s var(--smooth);position:relative}
.awards__item span:first-child{width:35%}
.awards__item span.year{width:auto;margin-left:auto;padding-left:2.4rem;text-align:right}
.awards__item img,.awards__item svg{z-index:1;transition:all .6s var(--smooth);margin-left:auto;display:block;position:relative}
.awards__item:first-child{border-top:1px solid #dedede}
/* QA-fix (mobile): three columns in a 358px row left the middle one 172px at
   1.8rem, which broke "Young Ambassador mentorship program" over four lines
   against a one-word first column. Trimming the type and rebalancing the split
   brings it back to two. */
@media screen and (max-width:991px){
  .awards__item{padding:3.2rem 0;font-size:1.5rem}
  .awards__item span{width:52%}
  .awards__item span:first-child{width:30%}
  .awards__item span.year{padding-left:1.2rem}
}
@media screen and (min-width:992px){
  .awards__item:hover:before{transform:scaleY(1)}
  .awards__item:hover span{color:#fff}
  .awards__item:hover span:first-child{transform:translate(2.4rem)}
  .awards__item:hover img,.awards__item:hover svg{filter:invert(1);transform:translate(-2.4rem)}
}
/* Footer contact — full viewport, centered, underlined 10vw mailto */
.about__contact{justify-content:center;align-items:center;height:100vh;display:flex}
.about__contact h2{letter-spacing:-.02em;font-size:10vw}
.about__contact h2 .char{text-decoration:underline}
.about__contact h2 a{color:#000}
@media screen and (max-width:991px){.about__contact h2{font-size:16vw}}

/* 13. Overview — full-screen vertical 3D carousel -----------------------------
   Track height is set from JS (slide count × 100vh). Slides enter from
   translateY(20vh) translateX(20%) rotateY(-50deg) blurred, exit to the
   mirrored .prev pose; .active settles to identity. 1.5s var(--custom). */
.overview{width:100%}
.overview .page-wrapper{height:100vh;position:relative;overflow:hidden}
.overview .carousel{width:100%;transition:transform 1.5s var(--custom)}
.overview .carousel__item{height:100vh;transform-style:preserve-3d;perspective:1000px;justify-content:center;align-items:center;display:flex;position:relative}
@media screen and (max-width:991px){.overview .carousel__item{text-align:center;padding-top:5vh;display:block}}
.overview .carousel__item__video{filter:blur(10px);width:1120px;max-width:50%;transition:all 1.5s var(--custom);background-color:#000;padding:2rem;transform:translateY(20vh) translate(20%) rotateY(-50deg)}
@media screen and (max-width:991px){.overview .carousel__item__video{max-width:80%;margin:0 auto}}
.overview .carousel__item__content{transition:all 1.5s var(--custom);filter:blur(20px);z-index:1;margin-left:5vw;max-width:35vw;position:relative;transform:translate(20vw)}
.overview .carousel__item__content h2{margin-bottom:2.4rem;font-size:4vw;line-height:1}
.overview .carousel__item__content h2 a{color:#000}
@media screen and (max-width:991px){
  .overview .carousel__item__content{width:100%;max-width:100%;margin-top:8vh;margin-left:0;padding:0 4vw}
  /* The reference keeps the arrows at top:50% and never collides with them
     because every slide there is titled with one short word. Ours run to
     "Diakonhjemmet Campus" / "Freyr Batterifabrikk", which at 8vw reach the
     full width and cross both arrows. Holding the centred title inside the
     span between them keeps the reference layout and the reference arrow
     position; only the measure changes. */
  .overview .carousel__item__content h2{max-width:62vw;margin-left:auto;margin-right:auto;font-size:8vw}
}
.overview .carousel__item.prev .carousel__item__video{filter:blur(10px);transform:translateY(-20vh) translate(-20%) rotateY(50deg)}
.overview .carousel__item.prev .carousel__item__content{filter:blur(20px);transform:translate(-20vw)}
.overview .carousel__item.active .carousel__item__video{filter:blur(0);transform:rotateX(0) rotateY(0)}
.overview .carousel__item.active .carousel__item__content{filter:blur(0);transform:translate(0,0)}
.overview .carousel__nav{pointer-events:none;justify-content:space-between;width:100%;padding:0 2vw;display:flex;position:absolute;top:50%;left:0;transform:translateY(-50%)}
.overview .carousel__nav button{appearance:none;cursor:pointer;pointer-events:initial;width:4.8rem;height:4.8rem;transition:all .6s var(--smooth);background-color:transparent;border:none;justify-content:center;align-items:center;display:flex;color:#000}
.overview .carousel__nav button svg{display:block}
.overview .carousel__nav button.disabled{filter:blur(5px);pointer-events:none}
.overview .carousel__nav__next{transform:rotate(180deg)}

/* 14. Work — editorial project grid ------------------------------------------
   Flex strip that starts 150vw wide and collapses to 100vw (js/work.js adds
   .animate), then collapses in height to 32vw / featured 65vh
   (.animate-height). Initial states are gated behind html.js so a no-JS
   render shows the resting grid. Media slots are .ph blocks (no images yet),
   so the hover scale targets .ph instead of img. */
.work .grid{position:relative}
html.js .work .grid:not(.end){pointer-events:none}
.work .grid__wrap{width:100%;height:100vh}
.work .grid__wrap__content{width:100%;height:100vh;transition:all 1.2s var(--custom);flex-wrap:wrap;display:flex}
.work .grid .featured{height:100%;transition:all 1.2s var(--custom);flex:70vw;padding:2vw 0 2vw 2vw;position:relative}
.work .grid .column{flex:23vw;height:100%;padding:2vw}
.work .grid .column__item{width:100%;height:calc(50% - 1vw);position:relative}
.work .grid .column__item:first-child{margin-bottom:2vw}
.work .grid .row{flex:100vw;justify-content:space-between;height:15vw;padding:0 2vw 2vw;display:flex}
.work .grid .row__item{width:calc(25% - 1.5vw);height:100%;position:relative}
@media screen and (min-width:992px){
  html.js .work .grid:not(.animate) .grid__wrap__content{width:150vw}
  .work .grid.animate.animate-height .grid__wrap__content{height:32vw}
  .work .grid.animate.animate-height .featured{height:65vh}
}
@media screen and (max-width:991px){
  .work .grid__wrap{height:auto}
  .work .grid__wrap__content{width:100%;height:auto}
  .work .grid .featured{flex:100%;width:100%;height:100vh;padding:0}
  .work .grid.animate .featured{padding:4vw}
  /* The intro's height collapse is not desktop-only on the reference: there the
     featured tile drops 100vh -> 65vh on a phone as well, once .animate-height
     lands, so the grid stops being a full screen you have to scroll past before
     the second tile appears. Measured on the reference at 390px: 664px during
     the intro, 432px after it. Ours held 100vh because the 65vh rule sat inside
     the (width>=992px) block. */
  .work .grid.animate.animate-height .grid__wrap__content{height:auto}
  .work .grid.animate.animate-height .featured{height:65vh}
  .work .grid .column{flex:100vw;height:auto;padding:0 4vw 4vw}
  .work .grid .column__item{height:50vw}
  .work .grid .column__item:first-child{margin-bottom:4vw}
  .work .grid .row{flex-wrap:wrap;height:auto;padding:0 4vw 4vw}
  .work .grid .row__item{width:100%;height:50vw;margin-bottom:4vw}
}

/* Project tiles (grid cards) */
.work .project{transition:all .6s var(--smooth);width:100%;height:100%;position:relative}
.work .project .image{width:100%;height:100%;transition:all .6s var(--smooth);position:relative;overflow:hidden}
.work .project .image:after{content:"";opacity:0;visibility:hidden;width:100%;height:100%;transition:all .6s var(--smooth);pointer-events:none;z-index:1;background-color:#000;display:block;position:absolute;top:0;left:0}
.work .project .ph,.work .project .media{transition:transform 1s var(--smooth)}
/* QA-fix: hover titles are capped to their tile (width:100% + wrap/hyphens) and
   non-featured tiles get a smaller 2.2vw so unbreakable words ("Batterifabrikk",
   "Diakonhjemmet") fit inside narrow column/row tiles instead of clipping at the
   viewport edge. The .ph__label fades out on hover so it never collides with
   the title (featured tile's 8vw title sat on top of it). */
.work .project__title{color:#fff;z-index:1;filter:blur(10px);opacity:0;pointer-events:none;transition:all .6s var(--smooth);width:100%;padding:0 1vw;font-size:4vw;line-height:1.1;text-align:center;overflow-wrap:break-word;hyphens:auto;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
.work .featured .project__title{font-size:8vw}
.work .column .project__title,.work .row .project__title{font-size:2.2vw}
.work .project .ph__label{transition:opacity .6s var(--smooth)}
.work .project__link{z-index:2;width:100%;height:100%;display:block;position:absolute;top:0;left:0}
@media screen and (min-width:992px){
  .work .project:hover .project__title{filter:blur(0);opacity:1}
  .work .project:hover .ph__label{opacity:0}
  .work .project:hover .image:after{opacity:.4;visibility:visible}
  .work .project:hover .ph,.work .project:hover .media{transform:scale(1.1)}
}
/* Touch: the tile titles come out of hover and stay on. The reference gates
   the whole reveal behind :hover, which on a phone means the work grid is a
   wall of photographs with nothing naming any of them — the one place the
   reference's desktop-only interaction leaves a real hole rather than just a
   flourish. This is the hover end-state held open: same centred white title,
   same scrim, dialled down (.28 instead of .4, and a step smaller) because it
   is now permanent rather than a half-second answer to a pointer, and the
   photograph still has to read through it.
   Gated on .grid.end so nothing appears until the intro animation has landed
   and the featured tile's "ARBEID" has blurred away — otherwise the two
   titles share that tile for the first 2.5s.
   (hover:none) rather than width alone so touch tablets past 991px — iPad in
   landscape is 1024 — get the labels too. */
@media (hover:none),screen and (max-width:991px){
  .work .grid.end .project__title{filter:blur(0);opacity:1}
  .work .grid.end .project .image:after{opacity:.28;visibility:visible}
  .work .grid.end .project .ph__label{opacity:0}
}
@media screen and (max-width:991px){
  .work .project__title{font-size:7vw}
  .work .featured .project__title{font-size:7vw}
  .work .column .project__title,.work .row .project__title{font-size:7vw}
}

/* Slot-machine title ("arbeid" over the featured tile). JS builds a column of
   4 stacked copies per letter; .anim rolls odd letters down from -400% and
   even letters up from +100% to -300% (1.6s --custom). .hide blurs it out
   once the intro is done. */
.title-multiple{padding:20vh 0}
.title-multiple__text{text-transform:uppercase;justify-content:center;height:14vw;margin:0 auto;font-size:14vw;line-height:1;display:flex;overflow:hidden}
.title-multiple__text>span{transition:transform 1.6s var(--custom);will-change:transform;height:100%;flex-direction:column;display:flex}
.title-multiple__text>span:nth-child(2n){transform:translateY(100%)}
.title-multiple__text>span:nth-child(odd){transform:translateY(-400%)}
.title-multiple__text.anim>span:nth-child(2n){transform:translateY(-300%)}
.title-multiple__text.anim>span:nth-child(odd){transform:translateY(0)}
.work .featured .title-multiple{pointer-events:none;color:#fff;height:18vw;padding:0;z-index:3;position:absolute;bottom:3vw;left:4vw;overflow:hidden}
html.js .work .featured .title-multiple{opacity:0}
.work .featured .title-multiple__text{height:22vw;font-size:22vw}
.work .featured .title-multiple__text.hide{filter:blur(10px);opacity:0;visibility:hidden;transition:all 1s var(--smooth)}
/* QA-fix (round 5): the Norwegian word is "ARBEID" — 6 letters where the
   reference had 4 ("WORK") — so at 22vw it ran ~11% past the featured tile's
   right edge and collided with the right-hand rail. Norwegian is scaled to
   0.84x, which lands it inside the tile with the same trailing gap English
   gets at 22vw; English keeps the reference size exactly. The wrapper height
   is scaled by the same factor so the vertical crop is unchanged.
   Everything here is vw-based, so one factor holds at every width. */
html[lang="nb"] .work .featured .title-multiple{height:15.1vw}
html[lang="nb"] .work .featured .title-multiple__text{height:18.5vw;font-size:18.5vw}
@media screen and (max-width:991px){
  .work .featured .title-multiple{height:26vw;top:8vw;bottom:auto;left:8vw}
  .work .featured .title-multiple__text{height:28vw;font-size:28vw}
  html[lang="nb"] .work .featured .title-multiple{height:21.8vw}
  html[lang="nb"] .work .featured .title-multiple__text{height:23.5vw;font-size:23.5vw}
}

/* Overview teaser ("Rask oversikt") with 3D coverflow ring (>=1401px only) */
.work__overview{align-items:center;height:800px;display:flex;position:relative}
.work__overview h2{font-size:4vw}
.work__overview p{width:40rem;max-width:100%;margin-bottom:2.4rem;font-size:1.8rem}
.work__overview .coverflow{pointer-events:none;align-items:center;width:70%;height:100%;display:flex;position:absolute;top:0;right:0}
.work__overview .coverflow__container{perspective:1000px;width:350px;height:220px;margin:0 auto;position:relative}
.work__overview .coverflow__carousel{width:100%;height:100%;transform-style:preserve-3d;transition:transform 1.2s var(--custom);position:absolute}
.work__overview .coverflow__item{text-align:center;background:#000;width:350px;height:220px;display:block;position:absolute;top:0;left:0}
.work__overview .coverflow__item:first-child{transform:rotateY(0) translateZ(350px)}
.work__overview .coverflow__item:nth-child(2){transform:rotateY(60deg) translateZ(350px)}
.work__overview .coverflow__item:nth-child(3){transform:rotateY(120deg) translateZ(350px)}
.work__overview .coverflow__item:nth-child(4){transform:rotateY(180deg) translateZ(350px)}
.work__overview .coverflow__item:nth-child(5){transform:rotateY(240deg) translateZ(350px)}
.work__overview .coverflow__item:nth-child(6){transform:rotateY(300deg) translateZ(350px)}
@media screen and (max-width:1400px){.work__overview .coverflow{display:none}}
@media screen and (max-width:991px){
  .work__overview{height:500px;padding-top:10vh}
  .work__overview h2{font-size:12vw}
  .work__overview p{width:80%}
}

/* "Andre prosjekter" archive list — rows with black scaleY fill hover.
   (The cursor-following .preview card from the original is intentionally
   skipped: no images yet.) */
.work .works{padding:10vh 0}
.work .works h2{font-size:4vw}
.work .works__item{border-bottom:1px solid #dedede;justify-content:space-between;align-items:center;padding:4rem 0;font-size:1.8rem;display:flex;position:relative}
.work .works__item:before{content:"";transform-origin:bottom;width:100%;height:100%;transition:transform .6s var(--smooth);background-color:#000;display:block;position:absolute;top:0;left:0;transform:scaleY(0)}
.work .works__item a{z-index:2;width:100%;height:100%;display:block;position:absolute;top:0;left:0}
.work .works__item span{z-index:1;transition:all .6s var(--smooth);position:relative}
.work .works__item span:first-child{width:50%}
.work .works__item span.year{margin-left:auto;padding-left:2.4rem;text-align:right}
.work .works__item svg{z-index:1;transition:all .6s var(--smooth);margin-left:2.4rem;display:block;position:relative}
.work .works__item:first-child{border-top:1px solid #dedede}
@media screen and (min-width:992px){
  .work .works__item:hover:before{transform:scaleY(1)}
  .work .works__item:hover span{color:#fff}
  .work .works__item:hover span:first-child{transform:translate(2.4rem)}
  .work .works__item:hover svg{filter:invert(1);transform:translate(-2.4rem)}
}
@media screen and (max-width:991px){
  .work .works{padding-top:10vh}
  .work .works h2{font-size:12vw}
  .work .works__item span:first-child{width:55%}
}

/* About teaser — tilted portrait that settles into place on scroll */
.work__about{text-align:center;overflow:hidden;padding:10vh 0 15vh}
.work__about__title{color:#000;margin-bottom:5vh;font-size:4vw;display:inline-block}
.work__about .image{width:25vw;height:50vh;transition:transform 2s var(--custom);margin:0 auto;overflow:hidden;transform:rotate(-12deg) translateY(15%) translateZ(0)}
html.js .work__about:not(.anim-triggered) .image{transform:rotate(-25deg) translateY(100%) translateZ(0)}
.work__about .image a{display:block;width:100%;height:100%}
/* The frame is portrait (25vw x 50vh) but the photograph is landscape
   (1920x1524), so cover crops ~200px of width at 1440. The reference anchors
   its portrait `right center`; that is wrong for this one, where the subject
   sits left of centre — anchoring right crops him to the frame's left edge and
   leaves the backdrop filling the right half. The centred crop crops evenly
   and lands him in the middle, so object-position is left to .full's default. */
.work__about .image .ph,
.work__about .image img{transition:transform 1s var(--smooth)}
@media screen and (min-width:992px){
  .work__about .image:hover .ph,
  .work__about .image:hover img{transform:scale(1.1)}
}
@media screen and (max-width:991px){
  .work__about__title{font-size:8vw}
  .work__about .image{width:80vw;height:50vh}
}

/* 15. Case (project detail) pages --------------------------------------------
   Hero enter: image closes from full-bleed into the clip-path inset block
   while the text column swings flat from a rotateY pose (2s --custom, js adds
   .animate). Pre-animation states are gated behind html.js :not(.animate) so
   the resting layout renders without JS. */
.case__intro{perspective:2000px;width:100%;height:100vh;position:relative;overflow:hidden}
.case__intro__img{clip-path:polygon(20% 0%,80% 0%,80% 100%,20% 100%);transform:scale(.6);transform-origin:100%;width:100%;height:100%;position:absolute;top:0;left:0}
.case__intro__text{z-index:2;flex-direction:column;justify-content:space-between;width:34%;height:50vh;display:flex;position:absolute;top:22vh;left:20vw}
/* QA-fix: long Norwegian single-word titles (DIAKONHJEMMET, BATTERIFABRIKK)
   crossed onto the clipped image (visible from x≈52vw). 3.4vw + max-width
   31.5vw keeps the longest word clear of the image at all desktop widths
   (DIAKONHJEMMET is the widest at 29.5vw) while leaving NORMORIA OPERA on one
   line; break-word is the safety net for any future title. hyphens:auto is
   deliberately off: it split OPERA as "OPE-RA" rather than wrapping at the
   space, which is worse than a second line. The "Amiamie Fallback" face keeps
   the pre-swap width identical, so the title never reflows as the font lands. */
.case__intro__text h1{max-width:31.5vw;font-family:Amiamie,"Amiamie Fallback","Helvetica Neue",Arial,sans-serif;font-size:3.4vw;font-weight:700;line-height:1;text-transform:uppercase;overflow-wrap:break-word;hyphens:none}
.case__intro__text h2{margin-top:5vh;padding-right:35%;font-size:3rem;line-height:1.2}
.case__intro__text .scroll{font-size:1.6rem}
html.js .case__intro:not(.animate) .case__intro__img{clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%);transform:scale(1)}
html.js .case__intro:not(.animate) .case__intro__text{filter:blur(10px);transform:rotateY(50deg) translate(-60vw) translateZ(10px) scale(2)}
html.js .case__intro:not(.animate) .case__intro__text h1{transform:translate(-5vw)}
html.js .case__intro:not(.animate) .case__intro__text h2{opacity:0;visibility:hidden;transform:translate(-5vw)}
html.js .case__intro:not(.animate) .case__intro__text .scroll{opacity:0;visibility:hidden}
.case__intro.animate .case__intro__img{transition:clip-path 2s var(--custom) .2s,transform 2s var(--custom) .4s}
.case__intro.animate .case__intro__text{transition:all 2s var(--custom) .6s}
.case__intro.animate .case__intro__text h1{transition:transform 1.3s var(--smooth) 1.6s}
.case__intro.animate .case__intro__text h2{transition:opacity 1.3s var(--smooth) 1.6s,visibility 1.3s var(--smooth) 1.6s,transform 1.3s var(--smooth) 1.8s}
.case__intro.animate .case__intro__text .scroll{transition:all .6s var(--smooth) 2.6s}
@media screen and (max-width:1600px){.case__intro__text h2{padding-right:20%}}
/* QA-fix (mobile): the multi-line h1 crossed the image band. Title now starts
   at 5vh (above the band), the band is lowered (visible ≈31–60vh), and the
   tagline is pinned just below it. h1 at 9.5vw keeps even DIAKONHJEMMET
   inside the 90% column without mid-word breaks. */
@media screen and (max-width:991px){
  .case__intro__img{clip-path:polygon(10% 26%,100% 26%,100% 62%,10% 62%);transform:scale(.8)}
  /* The column starts below the language pill rather than at 5vh. The pill is
     fixed at top:1.6rem and 4.4rem tall on touch, so it owns the top 6rem of
     the right-hand side — and a one-line title (NORMORIA OPERA, HI-FDIR) runs
     the full 90% column at 9.5vw and passed straight under it. 7rem clears the
     pill with a rem of air at every viewport height.

     The column bottom was 95vh, which put the "scroll" cue at y613-631 on a
     390x664 screen — inside the nav pill's own y592-640. The cue is anchored to
     the bottom of this column, so it was sitting behind the pill on every case
     page. The reference ends its column at 75vh and clears the pill by 94px;
     78vh here clears it by 74px, the difference being that our image band runs
     to 62% rather than the reference's 60%, so everything below it starts
     lower. h2 follows the band as before, 1vh under it rather than 6vh, which
     keeps the reference's rhythm of band -> subtitle -> cue and leaves the cue
     32px of air instead of 9px. */
  .case__intro__text{width:90%;top:7rem;height:calc(78vh - 7rem);left:4vw}
  .case__intro__text h1{max-width:100%;font-size:9.5vw}
  .case__intro__text h2{margin-top:0;padding-right:10%;font-size:1.4rem;position:absolute;top:calc(63vh - 7rem);left:0}
  .case__intro__text .scroll{font-size:1.2rem}
}

/* Metadata block — ADDITION vs. the original (which has no client/year table).
   Same visual language: hairlines #dedede, 1.4rem uppercase labels, 1.8rem values. */
/* QA-fix: was flex + space-between with 5 items (uneven gaps, last item pinned
   to the far right). Now a fixed 5-column grid (2 columns on mobile) so labels
   align on a clean editorial grid. */
.case__meta{padding:10vh 0}
.case__meta__row{border-top:1px solid #dedede;border-bottom:1px solid #dedede;padding:3.2rem 0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:0 2.4rem}
.case__meta__item{min-width:0}
.case__meta dt{font-size:1.4rem;text-transform:uppercase;letter-spacing:.05em}
.case__meta dd{margin-top:1.2rem;font-size:1.8rem;line-height:1.3}
@media screen and (max-width:991px){
  .case__meta{padding:6vh 0}
  .case__meta__row{padding:2.4rem 0;grid-template-columns:repeat(2,minmax(0,1fr));gap:2.4rem}
}

/* Image trio — wrap squeezes from 330% to 100% width on scroll */
.grid-images{padding:0 2vw;overflow:hidden}
.grid-images .wrap{justify-content:space-between;width:100%;transition:all 1.4s var(--custom);display:flex}
html.js .grid-images.anim:not(.anim-triggered) .wrap{width:330%;transition:none}
.grid-images__item{width:calc(33% - 1vw);height:20vw}
@media screen and (max-width:991px){
  .grid-images .wrap{flex-wrap:wrap}
  .grid-images__item{width:100%;height:60vw;margin-bottom:2vw}
}
/* Two-up variant: fewer, taller cells. Used where a cell has to be read rather
   than glanced at — e.g. the Eco Moyo frame drawings (.media--contain). */
.grid-images--pair .grid-images__item{width:calc(50% - .5vw);height:30vw}
@media screen and (max-width:991px){.grid-images--pair .grid-images__item{width:100%;height:60vw}}

/* Filmstrip variant: every item shares one height and keeps its own proportions,
   so a portrait clip sits narrow between landscape stills and nothing is cropped.
   Widths are intrinsic, so keep the combined ratios under ~4.3 or the row will
   run past the 96vw container. */
.grid-images--strip .wrap{align-items:flex-start;gap:0 1vw}
.grid-images--strip .grid-images__item{flex:0 0 auto;width:auto;height:22vw}
.grid-images--strip .media{width:auto;height:100%}
.grid-images--strip .full{width:auto;height:100%;object-fit:contain}
@media screen and (max-width:991px){
  .grid-images--strip .wrap{flex-wrap:wrap;gap:2vw 0}
  .grid-images--strip .grid-images__item{width:100%;height:auto;margin-bottom:0}
  .grid-images--strip .media{width:100%;height:auto}
  .grid-images--strip .full{width:100%;height:auto}
  /* The trio squeeze starts the row at 330% (line ~667). The reference keeps
     that on mobile too, and gets away with it because its items are a fixed
     60vw tall — the block's height is bounded however wide the row is. This
     variant stacks at the items' own aspect ratios instead (height:auto, so
     portrait clips aren't cropped), which makes height scale with that 330%:
     the strip rendered ~3.3x too wide AND ~5300px tall, clipped to the left
     third, and since checkAnims() waits for the block's *bottom* to come up
     it stayed that way for six screens of scrolling before snapping in.
     Squeezing a wrapped vertical stack is meaningless anyway, so the row
     starts at its resting width here and only animates on desktop. */
  html.js .grid-images--strip.anim:not(.anim-triggered) .wrap{width:100%}
}

/* Description paragraphs — 3.2rem with word-by-word blur reveal (main.js) */
.case__text{padding:10vh 0}
.case__text p{width:80rem;max-width:100%;font-size:3.2rem}
.case__text p+p{margin-top:3.2rem}
@media screen and (max-width:991px){.case__text p{width:100%;font-size:2.4rem}}

/* Black video band */
.case__video{background-color:#000;padding:20vh 0}
.case__video__frame{width:1080px;max-width:80%;margin:0 auto}
/* The band takes more than one frame when a project has both footage and
   drawings (Eco Moyo): stack them with air in between. */
.case__video__frame+.case__video__frame{margin-top:6vh}
/* Inset variant (Eco Moyo): the closing photo sits centred at half the viewport
   width and is shown whole at its natural 3:2 — the black band frames it rather
   than being filled by it. Padding is trimmed from the shared 20vh so the band
   still lands within a single screen at this size. */
.case__video--inset{padding:12vh 0}
.case__video__frame--inset{width:50%;max-width:50%}
.case__video__frame--inset .media{height:auto;aspect-ratio:3/2}
/* Full-bleed variant (Dora 2): the film is the band rather than sitting inside
   it — full viewport width and a full screen of height, so nothing of the black
   surround is left. Height is capped on small screens, where a 100vh clip would
   otherwise swallow the whole page on one scroll. */
.case__video--full{padding:0}
.case__video__frame--full{width:100%;max-width:100%;height:100vh;margin:0}
.case__video__frame--full .media{height:100%}
@media screen and (max-width:991px){
  .case__video__frame{max-width:90%}
  .case__video__frame--inset{width:90%;max-width:90%}
  .case__video__frame--full{width:100%;max-width:100%;height:70vh}
}

/* CTA row + credits */
.case__cta{padding:15vh 0}
.case__cta .container{flex-wrap:wrap;justify-content:space-between;align-items:center;display:flex}
.case__cta h2{letter-spacing:-.04em;font-size:6vw}
.case__cta .cta{border-radius:4rem;height:7.2rem;padding:0 4.8rem;font-size:2.4rem}
.case__cta .team{text-align:right;width:100%;margin-top:6rem}
@media screen and (max-width:991px){
  .case__cta h2{width:100%;margin-bottom:2rem;font-size:5rem}
  .case__cta .team{text-align:left}
}

/* "NESTE PROSJEKT" footer — next project's media shrinks to a 50vh square
   behind a vertical marquee of 6 title copies (10vw, 4s linear loop) */
.text-scroll{justify-content:center;height:100vh;display:flex;position:relative;overflow:hidden}
.text-scroll__img{z-index:1;width:50vh;height:50vh;transition:width 1.5s var(--custom),height 1.5s var(--custom);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
html.js .text-scroll.anim:not(.anim-triggered) .text-scroll__img{width:100%;height:100%;transition:none}
.text-scroll__img a{width:100%;height:100%;display:block}
.text-scroll .wrap{height:100%;overflow:hidden}
.text-scroll .text{text-transform:uppercase;font-size:10vw;line-height:1.5;text-align:center;animation:4s linear infinite marquee}
@keyframes marquee{to{transform:translateY(-15vw)}}

/* ==========================================================================
   16. Mobile hardening
   Everything here is scoped to touch/small screens; the desktop cascade above
   is untouched. Three problems, in order of how often they bite:

   a) Dynamic viewport height. Every full-bleed block in this design is sized
      in vh, and on iOS Safari / Chrome Android `vh` resolves against the
      *largest* viewport — the one with the toolbars retracted. On first paint
      the toolbar is out, so a 100vh hero is taller than the screen and its
      bottom (the "scroll" cue, the tagline pinned at 63vh) sits underneath the
      browser chrome; then the toolbar retracts on the first scroll and every
      vh box on the page resizes at once, which in a layout made of full-screen
      panels reads as the page lurching. `svh` is the small viewport — the
      toolbar-visible one — and it never changes, so the content always fits
      and nothing reflows mid-scroll. Behind @supports, so a browser without
      svh keeps exactly the vh behaviour it has now.
   b) Touch targets. The design's small controls are 21–32px tall. The boxes
      are grown with pseudo-element hit areas instead of padding so nothing
      moves: the pill still measures 3.2rem, the finger gets 44px.
   c) Overscroll. The overview carousel reads raw touchstart/touchend on
      window; without this the same drag also triggers pull-to-refresh and the
      rubber-band, so a swipe that should advance a slide bounces the page.
   ========================================================================== */

/* a) Dynamic viewport height -------------------------------------------- */
@supports (height:100svh) {
  @media screen and (max-width:991px) {
    .intro{min-height:100svh}
    .about .portrait{height:50svh}
    .about__contact{height:100svh}
    .work .grid__wrap__content{height:auto}
    .work .grid .featured{height:100svh}
    .work .grid.animate.animate-height .featured{height:65svh}
    .work__about .image{height:50svh}
    .case__intro{height:100svh}
    .case__intro__text{height:calc(78svh - 7rem)}
    .case__intro__text h2{top:calc(63svh - 7rem)}
    .case__video__frame--full{height:70svh}
    .text-scroll{height:100svh}
  }
  /* The overview carousel is a fixed, non-scrolling stack: the track is
     translated by whole viewport heights from JS (js/overview.js writes the
     same unit), so the slide box and the step have to agree. svh, not dvh —
     a unit that changes size underneath a translate would drift the stack. */
  .overview .page-wrapper{height:100svh}
  .overview .carousel__item{height:100svh}
}

/* b) Touch targets ------------------------------------------------------- */
@media (hover:none),screen and (max-width:991px) {
  /* The language pill grows outright rather than getting an invisible hit
     area: .lang__item__link is overflow:hidden (it clips the blur-flip), so a
     pseudo-element reaching past the link box would be cut off at exactly the
     edge it is meant to extend. 3.2rem -> 4.4rem also brings it nearer the
     4.8rem nav pill, which reads better as a pair on a small screen. */
  .lang__menu{height:4.4rem}
  .lang__menu:before,.lang__menu:after{border-radius:2.2rem}
  /* Inline text links — "Rask oversikt", the tel: number in the about column —
     are a single line of 1.4rem type, ~21px tall. These have no overflow
     clip, so an invisible box centred on the text does the job without
     moving anything. */
  .link-overview:before{content:"";position:absolute;top:50%;left:-.4rem;right:-.4rem;height:4.4rem;transform:translateY(-50%)}
}

/* c) Overscroll ---------------------------------------------------------- */
body[data-page="overview"]{overscroll-behavior-y:none}
