/* Project Page Styles */

/* CRYPT N2 and N3, Hugo Bijaoui's cipher typefaces. Declared here so they are
   only downloaded on a page that actually sets text in them. These are
   re-mapped copies: the page holds the text with its letters swapped, and
   each font draws the right letter for the swapped one, so the words read
   correctly on screen while copying or viewing the source gives gibberish. */
@font-face {
    font-family: 'CRYPT N2';
    src: url('fonts/tesseract-n2.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'CRYPT N3';
    src: url('fonts/tesseract-n3.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

/* A description set in a cipher face. The characters stay real text, so
   search engines and screen readers still read the words. */
.text-crypt-n2 {
    font-family: 'CRYPT N2', 'VG5000', sans-serif;
}

.text-crypt-n3 {
    font-family: 'CRYPT N3', 'VG5000', sans-serif;
}

/* A third cut of CRYPT N3, with the letters in their normal places, for words
   every visitor already knows. Set in the cipher cut, those words would give
   the key away. */
@font-face {
    font-family: 'CRYPT N3 NAV';
    src: url('fonts/tesseract-n3-nav.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

/* TESSERACT only: the name in the header and the menu links in CRYPT N3.
   Upper case because the face only draws capitals. The work's title, its
   specifications and the contact details stay in the site's typeface. */
.page-tesseract .site-title,
.page-tesseract .sidebar-nav a {
    font-family: 'CRYPT N3 NAV', 'VG5000', sans-serif;
    text-transform: uppercase;
}

/* The cipher paragraphs can't be selected, copied or long-pressed, wherever
   they sit on the page (the hover block and the "view more" text alike) */
.text-crypt-n2,
.text-crypt-n3 {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* "view more" opens the same text in CRYPT N2, set apart from the N3 one */
.page-tesseract .description-more {
    margin-top: 1.5rem;
}

/* No black outline around the photographs on TESSERACT and JULIA. Scoped to
   the body element so it outranks the annexe-page outline rule further down
   the file, which has the same weight otherwise and would win by coming later. */
body.page-tesseract .project-gallery img,
body.page-tesseract .project-gallery video,
body.page-julia .project-gallery img,
body.page-julia .project-gallery video {
    border: 0;
}

/* The portrait drawing closing the gallery: the frame hugs the image instead
   of spanning the row, so its black outline sits on the drawing rather than
   around empty bands. Centred, and capped by the screen in both directions. */
.page-tesseract .project-gallery > .gallery-full {
    width: auto;
    max-width: 100%;
    justify-self: center;
}

/* Two faces, one text. CRYPT N3 lies underneath and is always visible; the
   same text in CRYPT N2 sits exactly on top of it and only shows inside a
   square window that follows the pointer over the text. Both faces share
   the same advance widths, so the two copies wrap and align identically. */
.crypt-reveal {
    --lens: 9rem;
    position: relative;
    /* Not selectable: nothing to drag over, copy, or long-press on a phone */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

.crypt-reveal > p {
    margin: 0;
}

.crypt-reveal-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    visibility: hidden;
    /* A square, hard-edged window: a single solid tile, positioned by the
       script through --x and --y (the pointer, relative to the text) */
    -webkit-mask-image: linear-gradient(#000000, #000000);
    mask-image: linear-gradient(#000000, #000000);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: var(--lens) var(--lens);
    mask-size: var(--lens) var(--lens);
    -webkit-mask-position: calc(var(--x, 0px) - var(--lens) / 2) calc(var(--y, 0px) - var(--lens) / 2);
    mask-position: calc(var(--x, 0px) - var(--lens) / 2) calc(var(--y, 0px) - var(--lens) / 2);
}

.crypt-reveal.is-revealing .crypt-reveal-layer {
    visibility: visible;
}

/* Light pages, for the annexe artworks. Inverting the page chrome sets them
   apart from the pixel works, which all live on black. Only the chrome is
   touched — photographs and clips are left exactly as they are. */
body.theme-light {
    color: #000000;
    background-color: #ffffff;
}

.theme-light .project-content {
    background-color: #ffffff;
}

.theme-light .project-description,
.theme-light .project-description a,
.theme-light .view-more-btn,
.theme-light .spec-label,
.theme-light .spec-value,
.theme-light .gallery-category-title {
    color: #000000;
}

/* A plain band in place of the pixelated one. Same height and stacking, so
   the title and the toggle sit on it exactly as they do elsewhere. */
.header-plain {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--header-height);
    z-index: 40;
    pointer-events: none;
    background-color: #ffffff;
    /* Rule along the bottom edge: on white, the band and the page beneath it
       are the same colour and the header would otherwise have no boundary. */
    border-bottom: 2px solid #000000;
}

.theme-light .site-title {
    color: #000000;
    /* The shadow exists to hold white type over artwork; on a plain white
       band it would only smudge the letters. */
    text-shadow: none;
}

.theme-light .menu-toggle {
    border-color: #000000;
}

/* Outlines: on white, an image with a pale edge would otherwise dissolve into
   the page. Inside the box, since everything here is border-box. */
.theme-light .project-gallery img,
.theme-light .project-gallery video {
    border: 2px solid #000000;
}

body {
    font-family: 'VG5000', 'Arial', sans-serif;
}

.project-nav {
    position: fixed;
    top: 2rem;
    left: 2rem;
    z-index: 100;
}

.back-link {
    color: #ffffff;
    text-decoration: none;
    font-size: 1rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 0.5rem 1rem;
    border: 1px solid #ffffff;
    transition: background-color 0.3s ease, color 0.3s ease;
}

.back-link:hover {
    background-color: #ffffff;
    color: #000000;
}

.project-hero {
    position: relative;
    /* 100% rather than 100vw: the viewport width counts the scrollbar, and
       the page gained a horizontal scroll of that width on desktop */
    width: 100%;
    height: 100vh;
    overflow: hidden;
}

/* Pinned cover: it stays put while the content panel scrolls up over it, the
   same arrangement as the home page. Opt-in per page, so the project pages
   that still scroll normally are untouched. */
.project-hero.is-pinned {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    /* 100vw would count the scrollbar and push the page sideways */
    width: auto;
    height: var(--hero-height);
    z-index: 0;
}

/* Addressed as the pinned hero's sibling rather than with a class of its own:
   there is only ever one content panel, immediately after it. */
.project-hero.is-pinned + .project-content {
    position: relative;
    z-index: 1;
    margin-top: var(--hero-height);
}

.project-hero-image {
    width: 100%;
    height: 100%;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

/* Same job as the cover image, for the pages whose hero is a clip */
.project-hero-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    display: block;
    pointer-events: none;
}

/* Sound toggle, bottom right of a hero clip, level with the title at bottom
   left. A square like every control on the site, drawn as pixels, and stacked
   above the pixelated canvas so it stays usable while the cover breaks up. */
.hero-sound {
    position: absolute;
    right: 3rem;
    bottom: 3rem;
    z-index: 4;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #ffffff;
    background-color: rgba(0, 0, 0, 0.35);
    border: 2px solid #ffffff;
    cursor: pointer;
    transition: background-color 0.25s ease;
}

@media (hover: hover) {
    .hero-sound:hover {
        background-color: var(--pixel-accent);
    }
}

.hero-sound:focus-visible {
    outline: 2px solid #ffffff;
    outline-offset: 3px;
}

.hero-sound svg {
    width: 1.35rem;
    height: 1.35rem;
    display: block;
}

/* Muted shows the speaker crossed out; sound on shows it with waves */
.hero-sound .icon-on,
.hero-sound.is-on .icon-off {
    display: none;
}

.hero-sound.is-on .icon-on {
    display: inline;
}

.project-hero-title {
    position: absolute;
    bottom: 3rem;
    left: 3rem;
    font-size: 3rem;
    font-weight: 300;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #ffffff;
    /*text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);*/
    z-index: 1;
}

/* Subtitle under a hero title, e.g. TESSERACT (from macro to micro): same
   typeface, a step smaller, in the case it is written in */
.project-hero-subtitle {
    display: block;
    margin-top: 0.5rem;
    /* Tied to the title, with a floor so it stays legible on a phone */
    font-size: max(0.4em, 0.85rem);
    letter-spacing: 0.08em;
    text-transform: none;
}

.project-content {
    background-color: #000000;
    padding: 4rem 2rem;
}

.view-more-btn {
    display: inline-block;
    color: #ffffff;
    cursor: pointer;
    font-size: 0.9rem;
    letter-spacing: 0.1em;
    text-transform: lowercase;
    margin-top: 1rem;
    text-decoration: underline;
    transition: opacity 0.3s ease;
}

.view-more-btn:hover {
    opacity: 0.7;
}

.description-more {
    margin-top: 0;
}

.project-description {
    max-width: 800px;
    margin: 0 auto 4rem;
    font-size: 1.1rem;
    line-height: 1.8;
    color: #ffffff;
    text-align: left;
}

.project-description a {
    color: #ffffff;
    text-decoration: underline;
    transition: opacity 0.3s ease;
}

.project-description a:hover {
    opacity: 0.7;
}

.project-specifications {
    max-width: 800px;
    margin: 0 auto 4rem;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1.5rem;
}

.spec-item {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.spec-label {
    font-size: 0.9rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #ffffff;
    opacity: 0.7;
}

.spec-value {
    font-size: 1rem;
    color: #ffffff;
}

/* A value that is a link reads like the links in the description */
a.spec-value {
    text-decoration: underline;
    transition: opacity 0.3s ease;
}

a.spec-value:hover {
    opacity: 0.7;
}

.spec-value-container {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.github-link {
    display: flex;
    align-items: center;
    text-decoration: none;
    transition: opacity 0.3s ease;
    margin-left: auto;
}

.github-link:hover {
    opacity: 0.7;
}

.github-icon {
    width: 20px;
    height: 20px;
    display: block;
}

.gallery-category-title {
    max-width: 1400px;
    margin: 4rem auto 2rem;
    font-size: 2rem;
    font-weight: 300;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #ffffff;
    text-align: center;
}

.project-gallery {
    max-width: 1400px;
    margin: 0 auto 4rem;
    display: grid;
    /* minmax(0, …) rather than 1fr: a plain fr track never shrinks below its
       content, and large media pushed the page wider than the screen */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 2rem;
}

.project-gallery img,
.project-gallery video {
    width: 100%;
    height: 400px;
    object-fit: cover;
    display: block;
    /* No pointer: nothing opens on click any more, and promising one would
       be a cursor that leads nowhere. */
    transition: opacity 0.3s ease;
}

.project-gallery img:hover {
    opacity: 0.8;
}

/* gallery-wide only means "take the whole row". It carries no ratio, because
   the portrait strip below is full width too — giving this class a landscape
   ratio squashed that container and left its clips overflowing it. */
.project-gallery .gallery-wide {
    grid-column: 1 / -1;
    height: auto;
}

/* The ratio belongs to the landscape clips themselves, never to gallery-wide.
   Declared here rather than inline on each element: an inline ratio would
   outrank the phone override below and could only be beaten with !important.
   Every landscape clip in the project is within 6% of 16:9. */
.project-gallery > .gallery-landscape {
    aspect-ratio: 16 / 9;
}

/* Shown whole rather than cropped: no imposed ratio, and contain instead of
   cover. A tall image is capped so it cannot swallow the screen, and the
   letterboxing that leaves is invisible against the black page. */
.project-gallery > .gallery-full {
    height: auto;
    max-height: 85vh;
    /* No aspect-ratio here: it would override the one the browser derives
       from the width and height attributes, which is what reserves the
       image's space before it loads and stops the page jumping. */
    object-fit: contain;
}

/* Click-to-play frame. The wrapper is the grid item, so it carries the size
   the video used to have. */
.gallery-player {
    position: relative;
    overflow: hidden;
    /* Square side of the button; overridden per context below */
    --play-box: 3.2rem;
}

.project-gallery > .gallery-player {
    height: 400px;
}

.project-gallery > .gallery-wide.gallery-player {
    height: auto;
    --play-box: 4.6rem;
}

.project-gallery .gallery-player video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* A centred square rather than a full-frame overlay, so it reads as a
   control sitting on the clip. Square because that is the shape the whole
   site uses for a control. */
.gallery-play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--play-box);
    height: var(--play-box);
    padding: 0;
    border: 2px solid #ffffff;
    background-color: rgba(0, 0, 0, 0.45);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.25s ease, opacity 0.25s ease;
}

.gallery-play:hover {
    background-color: var(--pixel-accent);
}

/* Drawn with borders rather than loaded as an asset: crisp at any size, and
   no request. Proportional to the button so every size stays balanced. */
.gallery-play::before {
    content: '';
    width: 0;
    height: 0;
    border-style: solid;
    border-width: calc(var(--play-box) * 0.22) 0
                  calc(var(--play-box) * 0.22) calc(var(--play-box) * 0.36);
    border-color: transparent transparent transparent #ffffff;
    /* A triangle's optical centre sits left of its bounding box */
    margin-left: calc(var(--play-box) * 0.1);
}

/* Once running, the control steps aside but returns on hover to pause */
.gallery-player.is-playing .gallery-play {
    opacity: 0;
}

.gallery-player.is-playing .gallery-play:hover {
    opacity: 1;
}

/* Handed over to the browser's player: our square would sit on top of the
   native control bar and duplicate its pause button. */
.gallery-player.has-controls .gallery-play {
    display: none;
}

/* Same pseudo-element, rebuilt as a pair of bars while playing */
.gallery-player.is-playing .gallery-play::before {
    border: 0;
    width: calc(var(--play-box) * 0.12);
    height: calc(var(--play-box) * 0.42);
    margin-left: 0;
    background-color: #ffffff;
    box-shadow: calc(var(--play-box) * 0.24) 0 0 #ffffff;
    transform: translateX(calc(var(--play-box) * -0.12));
}

/* Portrait clips grouped into their own strip, three across, spanning the
   full width via gallery-wide. Rows wrap on their own past the third. */
.gallery-portraits {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 2rem;
}

.gallery-portraits .gallery-player {
    height: auto;
    aspect-ratio: 9 / 16;
}

/* Stills placed in a portrait strip. Only the clips get a wrapper from the
   script, so images need the ratio applied to them directly. */
.project-gallery .gallery-portraits img {
    height: auto;
    aspect-ratio: 9 / 16;
}

.gallery-image-top {
    object-position: top;
}

.gallery-video {
    background-color: #000000;
}

/* Credits closing the page, in the same register as the specifications near
   the top: small uppercase labels over the names, on the same 800px column. */
.project-credits {
    max-width: 800px;
    margin: 4rem auto 0;
    padding-top: 2rem;
    border-top: 1px solid rgba(255, 255, 255, 0.2);
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem 3rem;
}

.credit {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.credit-label {
    font-size: 0.9rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.7;
}

.credit-name {
    font-size: 1rem;
}

/* The separating rule would be white on white on the annexe pages */
.theme-light .project-credits {
    border-top-color: rgba(0, 0, 0, 0.2);
}

/* Related projects closing a page: a full-width list of the sibling works,
   one row each — square thumbnail, the work's name beside it, date and venue
   pushed to the far right. Ruled with the same pixel dots as Exhibitions. */
.related-projects {
    /* Square dots, as wide as the gaps between them. One tile per dot, sized
       in whole pixels, so every edge lands on a pixel and stays hard. */
    --dot: 6px;
    --pixel-rule: linear-gradient(to right, #ffffff 50%, transparent 50%);
    /* Out of the content panel's side padding, so the rules run edge to edge
       of the screen, then inset like the header */
    margin: 6rem -2rem 0;
    padding: 0 var(--edge-inset);
}

.related-title {
    margin-bottom: 2rem;
    font-size: clamp(1.4rem, 2vw, 2rem);
    font-weight: 300;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: #ffffff;
}

.related-list {
    list-style: none;
    position: relative;
    padding-top: var(--dot);
}

/* The rules are drawn as their own strips rather than as row backgrounds. A
   strip is cut to a whole number of dots and ends on a dot, so neither end of
   the line shows a sliver of one. Browsers without round() keep the full
   width, with only the right end possibly clipped. */
.related-list::before,
.related-item::after {
    content: '';
    position: absolute;
    left: 0;
    height: var(--dot);
    width: 100%;
    width: calc(round(down, 100% + var(--dot), var(--dot) * 2) - var(--dot));
    background: var(--pixel-rule) left top / calc(var(--dot) * 2) var(--dot) repeat-x;
    pointer-events: none;
}

.related-list::before {
    top: 0;
}

.related-item::after {
    bottom: 0;
}

/* Without the dotted rules: the PXL series pages (PXIL, PXIIL, PXIIIL) */
.related-projects-plain .related-list::before,
.related-projects-plain .related-item::after {
    display: none;
}

.related-item {
    display: grid;
    grid-template-columns: 12rem minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 2rem;
    position: relative;
    padding: 1.25rem 0 calc(1.25rem + var(--dot));
    color: #ffffff;
    text-decoration: none;
}

.related-thumb {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    display: block;
    transition: opacity 0.3s ease;
}

.related-name {
    font-size: clamp(1.2rem, 2vw, 2rem);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    transition: color 0.25s ease;
}

.related-meta {
    display: flex;
    gap: 2rem;
    text-align: right;
    font-size: clamp(0.9rem, 1.1vw, 1.1rem);
    letter-spacing: 0.05em;
}

.related-year {
    opacity: 0.7;
}

@media (hover: hover) {
    .related-item:hover .related-name {
        color: var(--pixel-accent);
    }

    .related-item:hover .related-thumb {
        opacity: 0.8;
    }
}

.related-item:focus-visible {
    outline: 2px solid #ffffff;
    outline-offset: 4px;
}

/* Responsive */
@media (max-width: 768px) {
    .project-nav {
        top: 1rem;
        left: 1rem;
    }

    .back-link {
        font-size: 0.8rem;
        padding: 0.4rem 0.8rem;
    }

    .project-hero-title {
        font-size: 1.5rem;
        bottom: 2rem;
        left: 2rem;
    }

    .hero-sound {
        right: 2rem;
        bottom: 2rem;
    }

    /* A tall screen shows only a narrow slice of a landscape clip. A page
       can move that slice onto its subject with --hero-focus-mobile. */
    .project-hero-video {
        object-position: var(--hero-focus-mobile, center);
    }

    .project-content {
        padding: 2rem 1rem;
    }

    /* A smaller window to match the narrower column */
    .crypt-reveal {
        --lens: 7rem;
    }

    /* The cipher text runs long on a phone: a smaller size keeps it compact */
    .page-tesseract .text-crypt-n2,
    .page-tesseract .text-crypt-n3 {
        font-size: 0.8rem;
    }

    /* Matches the panel's narrower side padding */
    .related-projects {
        margin: 4rem -1rem 0;
    }

    .related-item {
        grid-template-columns: 7rem minmax(0, 1fr) auto;
        column-gap: 1rem;
        padding: 1rem 0;
    }

    /* Date above venue, still against the right edge */
    .related-meta {
        flex-direction: column;
        gap: 0.2rem;
        font-size: 0.8rem;
    }
    
    .project-specifications {
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    .gallery-category-title {
        font-size: 1.5rem;
        margin: 3rem auto 1.5rem;
    }

    .project-gallery {
        grid-template-columns: 1fr;
        gap: 1rem;
        margin-bottom: 3rem;
    }
    
    .project-gallery img,
    .project-gallery video {
        height: 300px;
    }

    /* The wrapper is the grid item now, so it takes the height. */
    .project-gallery > .gallery-player {
        height: 300px;
        --play-box: 2.8rem;
    }

    /* Landscape clips turn square on a phone, where 16:9 across a narrow
       screen reads as a thin strip. Three classes deep on purpose: the base
       rule pinning wide players to height:auto has three too, and a shorter
       selector here would lose to it and leave the clip at its own ratio. */
    .project-gallery > .gallery-wide.gallery-landscape:not(.gallery-keep-ratio) {
        aspect-ratio: 1 / 1;
        height: auto;
    }

    /* One per row on a phone: three 9:16 clips side by side would be about
       120px wide each, too narrow to read anything in. */
    .gallery-portraits {
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    .gallery-portraits .gallery-player {
        --play-box: 3.2rem;
    }

}

