/* ==========================================================================
   Portfolio — Virtual Event Summit case study
   Figma: Portfolio / frames 98:4271 (branding) and 98:5062 (landing page),
   each 896 wide: 800 column + 48 padding
   ========================================================================== */

/* Fonts (self-hosted, SIL Open Font License) -------------------------------- */

/* Inter 3.19, the version the Figma file is set in. (Inter 4 is spaced a
   touch wider, which shifts where lines end.) */
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter-latin-700-normal.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Tokens -------------------------------------------------------------------- */

:root {
  --ink: #fff;
  --paper: #000;
  --muted: rgb(255 255 255 / 0.55);
  --line: #6e6e6e;
  --accent: #ff66f5;
  --chrome: #161616;

  --font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  --column: 800px;
  --gutter: 48px;
  --gap-section: 36px;
  --gap-block: 24px;
  --radius: 16px;
  --radius-panel: 12px;
}

/* Base ---------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  /* The hero's particle layer is as wide as the window, scrollbar and all.
     This keeps it from making the page scroll sideways. */
  overflow-x: clip;
  background: var(--paper);
  color: var(--ink);
  /* Figma's "auto" line height for Inter, rounded to whole pixels as Figma does. */
  font: 400 17px / 21px var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1,
h2,
h3,
p,
ul,
figure {
  margin: 0;
}

ul {
  padding: 0;
  list-style: none;
}

img,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

h3 {
  font-size: 21px;
  line-height: 25px;
}

/* Page layout --------------------------------------------------------------- */

.case {
  display: flex;
  flex-direction: column;
  gap: var(--gap-section);
  max-width: calc(var(--column) + 2 * var(--gutter));
  margin-inline: auto;
  padding: var(--gutter);
}

/* The page is two of these columns: the case study in <main>, then the other
   projects and the contacts in <footer>, the usual gap below it. The case
   study's column ends flush with its last section, and that is where the
   section menu stops (see .toc). */
main.case {
  position: relative;
  padding-bottom: 0;
}

footer.case {
  padding-top: var(--gap-section);
}

.case__group {
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
}

.case__project {
  font-size: 48px;
  line-height: 58px;
}

.case__title {
  font-size: 37.8px;
  line-height: 46px;
}

/* The mark in particles, live (particles/index.html). This is the box its
   still has in the Figma frame, 800 × 448, and the mark sits in it as it does
   in the still. */
.case__hero {
  position: relative;
  aspect-ratio: 800 / 448;
}

/* The particles are drawn on a much larger layer, so that the mark can explode
   past the box: the full width of the window and 2.75 boxes tall (three
   quarters of a box above it, one below), behind the text around it. The page
   inside is black too, like this one, so nothing flashes before it has loaded.

   The layer's middle is the middle of the mark, which is 6.75% of the box left
   of the box's own middle. The layer is wider by twice that, so it still
   reaches the window's right edge. Its top and bottom edges fade out, so
   particles leave it softly. Clicks pass through it to the page. */
.case__hero iframe {
  position: absolute;
  z-index: -1;
  top: -75%;
  left: 43.25%;
  width: calc(100vw + 13.5%);
  height: 275%;
  translate: -50% 0;
  border: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(transparent, #000 20%, #000 75%, transparent);
  mask-image: linear-gradient(transparent, #000 20%, #000 75%, transparent);
}

/* The still, shown where the particles can't be drawn (see index.html). Its
   export has the rounded corners baked in on white. Clipping one pixel inside
   them keeps that white from showing as a faint arc on the black page. */
.case__hero img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  clip-path: inset(1px round calc(var(--radius) - 1px));
}

.case__type {
  border-radius: var(--radius-panel);
}

/* A new part of the case study starts as far below the previous one as two
   of the Figma frames stacked: twice the page padding. */
.case__part {
  margin-top: calc(2 * var(--gutter) - var(--gap-section));
}

/* The outlined, rounded boxes the artwork sits in. */
.panel {
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background: var(--paper);
}

/* Logotype and mark --------------------------------------------------------- */

/* Three stages of the logo, centred in a 256px-tall panel. Each stage is a
   156px-tall column: artwork at the top, caption at the bottom. */
.logos {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 54.6px;
  min-height: 256px;
}

.logos__step {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  height: 156px;
}

.logos__step--early {
  width: 154.7px;
}

.logos__step--middle {
  width: 186px;
}

.logos__step--final {
  width: 262px;
}

.logos__step figcaption {
  color: var(--muted);
  line-height: 19.81px;
  text-align: center;
}

/* Illustration style -------------------------------------------------------- */

.illustrations {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding-bottom: 16px;
}

.illustrations__row {
  display: flex;
  justify-content: center;
  max-width: 100%;
}

/* Sizes are the ones in the Figma frame; below that the artwork in a row
   shrinks together, keeping its proportions. */
.illustrations__row img {
  flex: 0 1 auto;
  min-width: 0;
}

.illustrations__row--objects {
  align-items: flex-start;
  gap: 8px;
}

.illustrations__row--objects img {
  width: 230.807px;
}

.illustrations__row--objects img:first-child {
  width: 206.4px;
}

.illustrations__row--icons {
  align-items: center;
  gap: 20.48px;
}

.illustrations__row--icons img {
  width: 224px;
}

/* Colors -------------------------------------------------------------------- */

.colors {
  display: flex;
  justify-content: center;
  gap: 39px;
  padding: 40px 47px;
}

.swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  width: 120px;
  font-size: 14px;
  line-height: 0.729;
  text-align: center;
}

/* The sphere: a radial gradient lit from the upper left. */
.swatch::before {
  content: "";
  width: 110px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--sphere) border-box;
}

.swatch--black {
  --sphere: radial-gradient(67.25% 67.25% at 40.69% 30.03%,
      #333 0%, #1c1c1c 27.11%, #101010 40.67%, #050505 54.23%, #000 73.56%,
      #060606 76.86%, #0d0d0d 80.17%, #1a1a1a 86.78%, #333 100%);
}

/* Only the black sphere is outlined, to lift it off the black panel. */
.swatch--black::before {
  border: 1px solid var(--line);
}

.swatch--star {
  --sphere: radial-gradient(67.25% 67.25% at 40.69% 30.03%,
      #eec774 0%, #eba08c 13.56%, #e779a4 27.11%, #e452bc 40.67%, #e12bd4 54.23%,
      #cf1bf4 74.8%, #9218f4 87.4%, #7316f4 93.7%, #5414f4 100%);
}

.swatch--ocean {
  --sphere: radial-gradient(67.25% 67.25% at 40.69% 30.03%,
      #3bffff 0%, #2cd9fd 17.79%, #1db2fa 35.58%, #1c72fa 56.49%, #1b51f9 66.95%,
      #1a31f9 77.4%, #3334f9 80.23%, #4c37f9 83.05%, #7d3efa 88.7%,
      #af44fa 94.35%, #e04afa 100%);
}

/* Ads ----------------------------------------------------------------------- */

/* Three ads in a row, as an accordion. One is open: it is as wide as its
   image, so all of the image shows. The other two share what is left of the
   panel. The middle one is open to begin with, which is the Figma layout.
   Pointing at another ad, or tapping it, opens that one instead (see
   index.html).

   The images are never scaled past the panel's height, only cropped. A closed
   ad shows the part that faces the open one: the ads before it are cropped
   from the left and the ads after it from the right. And no ad gets more room
   than its image is wide, so an image that already fits is just shown whole.

   --ratio is the image's width over its height. index.html measures it from
   the file, so an export can be replaced by one of other proportions. */
.ads {
  --ad: 392px;

  display: flex;
  justify-content: center;
  gap: 8px;
}

.ads img {
  --whole: calc(var(--ad) * var(--ratio, 1));

  flex: 1 1 0px;
  min-width: 0;
  max-width: var(--whole);
  height: var(--ad);
  object-fit: cover;
  object-position: 100% 50%;
  transition: 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
  transition-property: flex-grow, flex-basis, object-position;
}

.ads .is-open {
  flex: 0 1 var(--whole);
  object-position: 50% 50%;
}

.ads .is-open ~ img {
  object-position: 0% 50%;
}

/* Landing page -------------------------------------------------------------- */

/* "Live page" link, as on the Communi page. */
.live-link {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--accent);
  font-size: 24px;
  font-weight: 700;
  line-height: 29px;
  text-decoration: none;
}

.live-link__text {
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-position: from-font;
}

/* Under the pointer the label drops its underline (and the globe turns, see
   below). */
.live-link:hover .live-link__text {
  text-decoration: none;
}

.live-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 4px;
}

/* The icon is a globe: a round window onto the world map. Hovering (or
   focusing) the link slides the map past underneath for exactly one turn,
   easing in and out. The script at the end of the page adds .is-turning to
   start a turn and removes it when the turn ends, so a turn always finishes
   even if the pointer has already left.

   Centred on the line, the globe is level with the capital L. But most of the
   label is lower-case, with descenders and an underline, so its weight sits
   lower and the globe looks high beside it. It is therefore set 1.5px lower,
   where its bottom edge meets the bottom of the underline. (That is 4px into
   the 29px line, so it also lands on whole pixels on any screen.) */
.live-link__globe {
  --map: 45px; /* width of one copy of world-map.svg */

  position: relative;
  top: 1.5px;
  flex: none;
  width: 24px;
  height: 24px;
  overflow: hidden;
  isolation: isolate;
  border-radius: 50%;
  background: var(--accent);
}

/* Three copies of the map side by side, moved along by exactly one copy, so
   the turn ends looking exactly as it began.

   The shared map is drawn blue on white. Here it has to be the accent colour
   on black, so the filter turns it into white land on a black sea, and
   multiplying that over the accent-coloured globe leaves accent land on
   black. (1.54 is what brings the inverted, greyed blue up to full white.) */
.live-link__globe::before {
  content: "";
  position: absolute;
  top: -0.84px;
  left: calc(-4.5px - var(--map));
  width: calc(3 * var(--map));
  height: 25px;
  background: url("../assets/world-map.svg") 0 0 / var(--map) 25px repeat-x;
  filter: invert(1) grayscale(1) brightness(1.54);
  mix-blend-mode: multiply;
}

.live-link.is-turning .live-link__globe::before {
  animation: globe-turn 1.43s ease-in-out;
}

/* The ring around the globe. */
.live-link__globe::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 2px solid var(--accent);
  border-radius: 50%;
}

@keyframes globe-turn {
  to {
    transform: translateX(calc(-1 * var(--map)));
  }
}

/* Muted, looping screen recordings. Each one fills the box its still has in
   the Figma frame, cropping to fit if the recording has other proportions. */
.clip {
  width: 100%;
  object-fit: cover;
  background: var(--chrome);
}

.clip--hero {
  aspect-ratio: 800 / 524.59;
}

.clip--prototype {
  aspect-ratio: 384 / 216;
}

.clip--blur {
  aspect-ratio: 400 / 226;
}

/* A recording that starts paused. The box is as wide as the recording's still
   in the Figma frame; a round button in its bottom left corner plays and
   pauses it, and so does a click on the recording (index.html). */
.player {
  position: relative;
}

.player--prototype {
  width: 384px;
}

.player--blur {
  width: 400px;
}

.player video {
  cursor: pointer;
}

.player__toggle {
  position: absolute;
  left: 12px;
  bottom: 12px;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: rgb(0 0 0 / 0.6);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.player__toggle[hidden] {
  display: none;
}

.player__toggle:hover {
  background: rgb(0 0 0 / 0.85);
}

.player__toggle:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* The icon is drawn with borders: a triangle to play, nudged right so that it
   looks centred in the circle… */
.player__toggle::before {
  content: "";
  margin-left: 3px;
  border-style: solid;
  border-width: 7px 0 7px 12px;
  border-color: transparent transparent transparent currentColor;
}

/* …and two bars to pause. */
.is-playing .player__toggle::before {
  width: 12px;
  height: 14px;
  margin-left: 0;
  border-width: 0 4px;
  border-color: currentColor;
}

/* A recording framed as a browser window. The outline is drawn over the
   content, as Figma draws its inside strokes, so the recording runs the full
   width of the column. */
.browser {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-panel);
}

.browser::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--line);
  border-radius: inherit;
  pointer-events: none;
}

/* The title bar: three window buttons and an address field. */
.browser__bar {
  position: relative;
  height: 22px;
  border-bottom: 1px solid var(--line);
  background: var(--chrome);
}

.browser__bar::before,
.browser__bar::after {
  content: "";
  position: absolute;
  top: 6px;
  height: 10px;
  border-radius: 5px;
  background: var(--line);
}

.browser__bar::before {
  left: 6px;
  width: 10px;
  box-shadow: 13px 0 var(--line), 26px 0 var(--line);
}

.browser__bar::after {
  left: 50%;
  width: 32.25%;
  translate: -50% 0;
}

/* A recording beside the paragraph that describes it. */
.split {
  display: flex;
  align-items: center;
  gap: 36px;
}

.split p {
  flex: 1 1 0;
  min-width: 0;
}

.split .player {
  flex: none;
}

/* The ticket itself, live (ticket/index.html). It follows the pointer while
   the pointer is over the panel. The page inside is black too, like the
   panel, so nothing flashes before it has loaded. */
.ticket-demo {
  position: relative;
  height: 448px;
}

.ticket-demo iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* A hint on how to play with the ticket, in the panel's bottom right corner:
   16px in from the panel's edge, one of which is the panel's border. It lies
   over the frame and lets the pointer through to the ticket.
   Figma: 251:2561 */
.ticket-hint {
  position: absolute;
  right: 15px;
  bottom: 15px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  line-height: 17px;
  white-space: nowrap;
  pointer-events: none;
}

.ticket-hint__finger {
  display: none;
}

/* On small screens (the phone layout below) the hint names the finger instead,
   from the bottom left corner. So it does on touch screens of any size, which
   have no mouse to move. (index.html swaps the icon on the same query.) */
@media (max-width: 599.98px), (hover: none), (pointer: coarse) {
  .ticket-hint {
    right: auto;
    left: 15px;
  }

  .ticket-hint__mouse {
    display: none;
  }

  .ticket-hint__finger {
    display: inline;
  }
}

/* Export fringes -------------------------------------------------------------- */

/* These layers have fractional sizes in Figma, so their exports end in a
   one-pixel light fringe on the right and/or bottom edge. Trim it off. */
.logos__step--early img {
  clip-path: inset(0 1px 0 0);
}

.illustrations__cover {
  clip-path: inset(0 1px 1px 0);
}

/* These three sit at fractional positions, where the fringe can smear across
   a second pixel on standard-density screens. */
.illustrations__row--objects img {
  clip-path: inset(0 2px 2px 0);
}

/* Back link ----------------------------------------------------------------- */

/* "← Home" at the top of the column, leading to the home page, with 32px
   between it and whatever comes first on the page. The arrow is
   assets/icon-back.svg used as a mask, so it takes the colour of the text.
   The padding only makes the link easier to hit; the margins take it back out
   of the layout (and turn the usual gap between sections into 32px). */
.back {
  --hit: 8px;

  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-block: calc(-1 * var(--hit)) calc(32px - var(--gap-section) - var(--hit));
  padding-block: var(--hit);
  color: inherit;
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  transition: opacity 0.15s ease;
}

.back::before {
  content: "";
  flex: none;
  width: 10px;
  height: 11px;
  background: currentColor;
  -webkit-mask: url("../assets/icon-back.svg") center / contain no-repeat;
  mask: url("../assets/icon-back.svg") center / contain no-repeat;
}

.back:hover {
  opacity: 0.6;
}

.back:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 2px;
}

/* Section menu -------------------------------------------------------------- */

/* A short list of the page's parts, beside the column on large screens: its
   right edge 40px from the column's left edge, 24px between the items. The
   part being read is at full opacity and the others at 60%; the script at the
   end of the page keeps aria-current on the right link.

   The menu keeps to the middle of the window for as long as the case study
   lasts. When the end of the case study comes up to the menu's bottom edge,
   the menu stops there and leaves with the page. This is position: sticky:
   .toc is a rail as tall as the case study, and the links stick inside it, so
   they cannot go past its end.

   (To try it on the other side of the column, swap "right" for "left" in .toc
   and "flex-end" for "flex-start" in .toc__links.) */
.toc {
  position: absolute;
  top: 0;
  bottom: 0;
  right: calc(100% - var(--gutter) + 40px);
  z-index: 1;
  display: none;
  pointer-events: none;
}

/* To centre the menu, sticky needs the menu's height, which is worked out
   here: --links lines of text with a gap between them. Change --links when a
   link is added to the menu or taken out of it. */
.toc__links {
  --links: 2;
  --line: 15px;
  --gap: 24px;
  --height: calc(var(--links) * var(--line) + (var(--links) - 1) * var(--gap));

  position: sticky;
  top: calc(50vh - var(--height) / 2);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--gap);
  font-size: var(--line);
  line-height: 1;
  pointer-events: auto;
}

.toc a {
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
  opacity: 0.6;
  transition: opacity 0.15s ease;
}

.toc a:hover,
.toc a[aria-current="true"] {
  opacity: 1;
}

.toc a:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 2px;
  opacity: 1;
}

/* Only where there is room for it beside the column. */
@media (min-width: 1200px) {
  .toc {
    display: block;
  }
}

/* A link in the menu brings its heading to the usual distance from the top. */
.case__title {
  scroll-margin-top: var(--gutter);
}

html {
  scroll-behavior: smooth;
}

/* See next ------------------------------------------------------------------ */

/* The other two projects, at the very end of the page: a small label over two
   pictures, each a link to its project. It is set as far below what comes
   before it as a new part of the case study (.case__part, in index.html). */
.next__label {
  font-size: 15px;
  font-weight: 400;
  line-height: 18px;
}

/* 388 × 166 each in Figma, 24px apart. The two share the row, and stack once
   there is less than 200px for each (a phone held upright). The pictures
   (assets/preview-*.jpg) are plain rectangles; the card rounds their corners. */
.next__cards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-block);
}

.next__card {
  flex: 1 1 200px;
  overflow: hidden;
  border-radius: 12px;
}

.next__card img {
  width: 100%;
  transition: transform 0.4s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* The same lift as the cards on the home page. */
.next__card:hover img {
  transform: scale(1.025);
}

.next__card:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

/* Contacts ------------------------------------------------------------------ */

/* What I am open to, with the ways to get in touch right under it, as on the
   home page. */
.availability {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.availability__status {
  font-size: 15px;
  line-height: 18px;
}

/* Email, WhatsApp and LinkedIn, as on the home page: an icon and an underlined
   label each, in a row. The padding only makes the links easier to hit; the
   list's negative margin takes it back out. */
.contacts {
  --hit: 8px;

  display: flex;
  flex-wrap: wrap;
  gap: 0 24px;
  margin-block: calc(-1 * var(--hit));
}

.contact {
  display: flex;
  align-items: center;
  gap: 5px;
  padding-block: var(--hit);
  color: inherit;
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  transition: opacity 0.15s ease;
}

/* The icons (assets/home) are drawn in black; inverted, they are white. */
.contact__icon {
  flex: none;
  width: 14px;
  height: 14px;
  filter: invert(1);
}

.contact__label {
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-position: from-font;
}

.contact:hover {
  opacity: 0.6;
}

.contact:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 2px;
}

/* Responsive ---------------------------------------------------------------- */

/* Tablets: the three logo stages no longer fit at their full spacing. */
@media (max-width: 839.98px) {
  .logos {
    flex-wrap: wrap;
    gap: 32px 40px;
    padding: 49px 24px;
  }

  /* The recordings beside text take half the row. */
  .split {
    gap: 24px;
  }

  .split .player {
    flex: 0 1 50%;
    min-width: 0;
  }
}

/* Phones. */
@media (max-width: 599.98px) {
  :root {
    --gutter: 20px;
  }

  .case {
    padding-block: 32px;
  }

  .case__project {
    font-size: 34px;
    line-height: 41px;
  }

  .case__title {
    font-size: 28px;
    line-height: 34px;
  }

  /* The logo stages stack. */
  .logos {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 32px;
    padding: 32px 16px;
  }

  .illustrations__row {
    padding-inline: 12px;
  }

  .illustrations__row--icons {
    gap: 8px;
  }

  /* The spheres shrink with the panel and their labels may wrap. */
  .colors {
    gap: 12px;
    padding: 32px 16px;
  }

  .swatch {
    flex: 0 1 120px;
    min-width: 0;
    line-height: 17px;
  }

  .swatch::before {
    width: min(110px, 100%);
  }

  /* The open ad takes most of the panel; the other two just peek in. */
  .ads {
    --ad: calc((100vw - 2 * var(--gutter)) * 0.8);
  }

  .case__part {
    margin-top: 28px;
  }

  /* Recording and text stack, each the full width of the column, the text
     first. */
  .split {
    flex-direction: column;
    align-items: stretch;
    gap: var(--gap-block);
  }

  .split p,
  .split .player {
    flex: none;
    width: 100%;
  }

  .split p {
    order: -1;
  }

  /* A square, so the ticket is as large as the column allows. */
  .ticket-demo {
    height: auto;
    aspect-ratio: 1;
  }

  /* The contacts stack, as on the home page. */
  .contacts {
    flex-direction: column;
    align-items: flex-start;
  }
}
