/**
 * The motion elements.
 *
 * Every effect has to look finished with the script blocked, so nothing here
 * hides anything that JavaScript is expected to bring back. The one exception
 * is the split reveal, which needs a mask - and that one un-hides itself if
 * the script never runs.
 */

.ds-fx{ --fx-line:color-mix(in srgb, var(--ds-text,#f5f5f5) 16%, transparent);
  --fx-fill:color-mix(in srgb, var(--ds-text,#f5f5f5) 6%, transparent) }

/* ---------------------------------------------------------- magnetic */
.ds-mag{ display:inline-grid; place-items:center; padding:18px 40px; border-radius:999px;
  border:1px solid currentColor; text-decoration:none; color:inherit;
  font-size:clamp(.95rem,1.2vw,1.1rem); letter-spacing:.02em; will-change:transform }
.ds-mag:hover{ background:var(--fx-fill) }
.ds-mag span{ display:block; will-change:transform }

/* --------------------------------------------------------- proximity */
.ds-fx-proximity{ display:grid; grid-template-columns:repeat(var(--ds-cols,8),1fr);
  gap:clamp(8px,1.4vw,18px); padding:clamp(16px,3vw,40px) 0 }
.ds-fx-proximity i{ aspect-ratio:1; display:grid; place-items:center; opacity:.35;
  border-radius:6px; background:var(--fx-fill); font-style:normal;
  font-size:clamp(.6rem,1vw,.85rem); text-align:center; will-change:transform }

/* ------------------------------------------------------ cursor trail */
/* The trail itself is fixed to the window, so the element has nothing in it.
   It still needs to be somewhere on the page, or a page whose only content is
   a cursor trail is a blank page with no hint that anything is there. */
.ds-fx-cursortrail{ position:relative; min-height:clamp(220px,32vw,380px);
  border:1px dashed var(--fx-line); border-radius:10px; display:grid;
  place-items:center }
.ds-fx-cursortrail::before{ content:'Move the pointer'; font-size:.9rem;
  letter-spacing:.14em; text-transform:uppercase; opacity:.4 }
.ds-trail{ position:fixed; top:0; left:0; z-index:9998; pointer-events:none;
  width:14px; height:14px; margin:-7px 0 0 -7px;
  background:var(--ds-trail-shadow, currentColor);
  border-radius:50%; will-change:transform }
.ds-trail.ring{ background:none; border:1px solid var(--ds-trail-shadow, currentColor) }
.ds-trail.square{ border-radius:2px }

/* ------------------------------------------------------- image trail */
/* overflow:hidden so a picture thrown off the edge is clipped by the element
   rather than widening the page. The pictures are absolute inside it. */
/* No border, no background, no cursor of its own - an element brings only what
   it was asked to bring. Height is the one thing it must have: a box with no
   height has nothing to move a pointer across. */
/* The element does not clip - its clip layer does. Words set to the viewport
   are wider than the column this sits in, and an element clipping everything it
   holds would cut them off at its own edges. */
.ds-fx-imagetrail{ position:relative; isolation:isolate;
  display:grid; place-items:center }
.ds-it-clip{ position:absolute; inset:0; display:block; overflow:hidden;
  z-index:0; pointer-events:none }
.ds-fx-imagetrail[data-tall="short"] { min-height:clamp(200px,26vw,320px) }
.ds-fx-imagetrail[data-tall="medium"]{ min-height:clamp(320px,44vw,560px) }
.ds-fx-imagetrail[data-tall="tall"]  { min-height:clamp(460px,64vw,760px) }
/* svh, not vh: on a phone vh is the tallest the window ever gets, so a full
   screen element is taller than the screen until the address bar has gone. */
.ds-fx-imagetrail[data-tall="full"]  { min-height:100vh; min-height:100svh }
.ds-fx-imagetrail:not([data-tall]){ min-height:clamp(320px,44vw,560px) }

/* Only written when there are words. Inherits size and color unless the
   element sets them. */
.ds-it-say{ pointer-events:none; text-align:center; max-width:24ch }

/* The page's own sizes, not numbers of its own. Line height comes with each
   one: a heading size on body leading is a heading with gaps in it. */
.ds-it-say[data-says="h1"]{ font-size:var(--ds-h1-size); line-height:var(--ds-h1-lh,1em) }
.ds-it-say[data-says="h2"]{ font-size:var(--ds-h2-size); line-height:var(--ds-h2-lh,1em) }
.ds-it-say[data-says="h3"]{ font-size:var(--ds-h3-size); line-height:var(--ds-h3-lh,1em) }
.ds-it-say[data-says="h4"]{ font-size:var(--ds-h4-size); line-height:var(--ds-h4-lh,1em) }
.ds-it-say[data-says="h5"]{ font-size:var(--ds-h5-size); line-height:var(--ds-h5-lh,1em) }
.ds-it-say[data-says="h6"]{ font-size:var(--ds-h6-size); line-height:var(--ds-h6-lh,1em) }
.ds-it-say[data-says="p"] { font-size:var(--ds-p-size);  line-height:var(--ds-p-lh,1.4em) }

/* Full width is a fit rather than a size: effects.js measures the words and
   sets the size that puts them edge to edge. One line, no wrapping cap, and
   nothing here that would fight the number it writes.
 *
 * Everything but the size comes from the Full width text row in Typography -
 * the same row the element and the footer block read, because all three are the
 * same thing: one line of type across the width it is given. */
/* On its own layer. Type this size is expensive to paint, and inside a smooth
   scroller the whole page is a moving surface - so every frame of a scroll was
   repainting a viewport of 200px letterforms. Promoted, it is rasterized once
   and moved about, which is what a compositor is for. */
.ds-it-say[data-says="full"]{ display:inline-block; max-width:none;
  white-space:nowrap;
  font-family:var(--ds-font-fullwidth, var(--ds-font-heading));
  font-weight:var(--ds-fullwidth-weight,400);
  text-transform:var(--ds-fullwidth-case,none);
  letter-spacing:var(--ds-fullwidth-track,-0.02em);
  line-height:var(--ds-fullwidth-lh,1);
  transform:translateZ(0); will-change:transform }

/* Out of the column and across the window. The offset and the width are
   measured by effects.js - left:0 here is only a starting point, so a browser
   with the script blocked still gets the words in the right place rather than
   somewhere arbitrary. */
.ds-it-say[data-says="full"][data-sayw="viewport"]{
  position:absolute; left:0; top:50%; transform:translate3d(0,-50%,0);
  text-align:center }

.ds-it-shot{ position:absolute; top:0; left:0; display:block;
  pointer-events:none; will-change:transform; border-radius:8px;
  box-shadow:0 20px 50px -20px rgba(0,0,0,.7) }
/* Matched on the attribute the renderer actually writes. These were class
   selectors, and the box carries data-size - so every trail drew its pictures
   at whatever size they happened to be. */
.ds-fx-imagetrail[data-size="small"]  .ds-it-shot{ width:clamp(70px,8vw,110px) }
.ds-fx-imagetrail[data-size="medium"] .ds-it-shot{ width:clamp(110px,12vw,170px) }
.ds-fx-imagetrail[data-size="large"]  .ds-it-shot{ width:clamp(150px,17vw,240px) }
/* A trail saved before the size setting existed still needs a sane width. */
.ds-it-shot{ width:clamp(110px,12vw,170px) }

/* -------------------------------------------------------------- dock */
.ds-fx-dock{ display:flex; gap:14px; align-items:flex-end; padding:24px 0 34px }
.ds-align-center.ds-fx-dock{ justify-content:center }
.ds-align-right.ds-fx-dock{ justify-content:flex-end }
.ds-dock-item{ display:block; width:clamp(44px,6vw,74px); flex:0 0 auto;
  transform-origin:50% 100%; will-change:transform }
.ds-dock-item img{ width:100%; height:auto; display:block; border-radius:12px }

/* ----------------------------------------------------------- counter */
.ds-fx-counter{ display:flex; flex-wrap:wrap; gap:clamp(24px,5vw,70px) }
.ds-align-center.ds-fx-counter{ justify-content:center }
.ds-align-right.ds-fx-counter{ justify-content:flex-end }
.ds-count b{ display:block; font-size:clamp(2.6rem,7vw,5.5rem); line-height:1;
  font-variant-numeric:tabular-nums; font-weight:500;
  color:var(--ds-counter-num, inherit) }
.ds-count span{ display:block; margin-top:8px; font-size:.9rem; letter-spacing:.08em;
  text-transform:uppercase;
  /* Its own colour where one is set, and the old faded ink where it is not -
     an opacity and a colour together would tint whatever was chosen. */
  color:var(--ds-counter-label, inherit);
  opacity:var(--ds-counter-label-fade, .6) }

/* ------------------------------------------------------ text effects */
/* The display size belongs to the headings, not to the class.
   This set clamp(1.8rem,5vw,4.4rem) on .ds-fx-text whatever tag it sat on, so
   choosing p for a split reveal still rendered at four and a half rem - the
   Level control swapped the element and changed nothing anybody could see. A
   paragraph now reads at the size the page sets, which is why you would pick
   it. */
.ds-fx-text{ margin:0 }
h1.ds-fx-text,h2.ds-fx-text,h3.ds-fx-text{
  font-size:clamp(1.8rem,5vw,4.4rem); line-height:1.06 }
p.ds-fx-text{ font-size:inherit; line-height:inherit }
.ds-fx-splitreveal .ds-splitline{ overflow:hidden }
.ds-caret{ display:inline-block; width:2px; height:.9em; margin-left:3px;
  background:currentColor; vertical-align:-.08em; animation:ds-blink 1s steps(1) infinite }
@keyframes ds-blink{ 50%{ opacity:0 } }

/* ---------------------------------------------------------- card stack */
.ds-fx-cardstack{ display:grid; gap:var(--fx-card-gap, clamp(30px,6vw,90px)) }
.ds-card-item{ margin:0; border-radius:14px; overflow:hidden; background:var(--fx-fill);
  will-change:transform }
.ds-card-item img{ width:100%; height:auto; display:block }

/* One shape for every card.

   Unset the cards are whatever their pictures are, which on a stack of mixed
   uploads meant each one a different height as it went past. Given a ratio the
   picture fills it and is cropped from the middle - the alternative is letter
   boxing, which is a grey bar somebody has to explain. */
/* Both kinds of card: a gallery picture in a figure, and a blog post whose
   image sits in its own wrapper. Same stack, same crop. */
.ds-fx-cardstack[data-crop="16:9"] :is(.ds-card-item, .ds-post-card-img) img{ aspect-ratio:16/9; object-fit:cover; height:100%; width:100% }
.ds-fx-cardstack[data-crop="3:2"]  :is(.ds-card-item, .ds-post-card-img) img{ aspect-ratio:3/2;  object-fit:cover; height:100%; width:100% }
.ds-fx-cardstack[data-crop="4:3"]  :is(.ds-card-item, .ds-post-card-img) img{ aspect-ratio:4/3;  object-fit:cover; height:100%; width:100% }
.ds-fx-cardstack[data-crop="1:1"]  :is(.ds-card-item, .ds-post-card-img) img{ aspect-ratio:1/1;  object-fit:cover; height:100%; width:100% }
.ds-card-item figcaption{ padding:16px 20px; font-size:.9rem; opacity:.65 }

/* A card has to fit the window it is pinned into.
 *
 * The stack pins each card from 12% down to 12% up, which is about three
 * quarters of the screen - but a card is its picture plus its title and
 * standfirst underneath, and that came to 739px against a 391px window on a
 * short viewport. The title sat 675px down, so it was below the fold for the
 * whole time the card was held, and the only sight of it was as the card shrank
 * and faded to 45% on its way out. Hence a title that is never readable.
 *
 * The card is capped at the pinned window and laid out as a column: the words
 * take the room they need and the picture takes what is left. object-fit means
 * the picture crops rather than squashes, which it was already doing for the
 * fixed ratios above.
 *
 * svh rather than vh, so a phone's shrinking address bar does not change the
 * cap mid-scroll and re-run the whole layout. */
.ds-fx-cardstack [data-card]{
  display:flex;
  flex-direction:column;
  max-height:76svh;
  overflow:hidden;
}

.ds-fx-cardstack [data-card] :is(.ds-post-card-img, .ds-card-item-pic, figure){
  flex:1 1 auto;
  min-height:0;
  overflow:hidden;
}

.ds-fx-cardstack [data-card] :is(.ds-post-card-img, .ds-card-item-pic, figure) img{
  width:100%;
  height:100%;
  object-fit:cover;
}

/* The words never shrink - they are the point of the card. */
.ds-fx-cardstack [data-card] :is(.ds-post-card-body, figcaption){
  flex:0 0 auto;
}

/* -------------------------------------------------- horizontal gallery */
.ds-fx-hgallery{ overflow:hidden }
.ds-hg-track{ display:flex; gap:clamp(14px,2vw,30px); width:max-content; will-change:transform }
.ds-hg-item{ flex:0 0 auto; width:clamp(240px,38vw,560px) }
.ds-fx-hgallery[data-size="small"] .ds-hg-item{ width:clamp(180px,24vw,340px) }
.ds-fx-hgallery[data-size="large"] .ds-hg-item{ width:clamp(300px,52vw,760px) }
.ds-hg-item img{ width:100%; height:auto; display:block; border-radius:10px }

/* A caption travelling with its own slide. flex+column so "over" is a reorder
   rather than absolute positioning, which would sit on top of the picture and
   need a scrim to stay readable. */
.ds-hg-item{ display:flex; flex-direction:column; gap:12px }
.ds-fx-hgallery[data-capat="over"] .ds-hg-item{ flex-direction:column-reverse }
.ds-hg-say{ font-size:.9rem; line-height:1.5; opacity:.7 }

/* ----------------------------------------------------- horizontal text */
.ds-fx-htext{ overflow:hidden }
.ds-htext-line{ display:inline-block; white-space:nowrap; will-change:transform;
  font-size:clamp(3rem,13vw,12rem); line-height:.95; letter-spacing:-.02em }

/* ------------------------------------------------------ pinned panels */
.ds-panel-slide{ min-height:100svh; display:grid; place-items:center;
  background:var(--ds-bg,#0b0b0c) center/cover no-repeat; position:relative }
.ds-panel-slide h3{ margin:0; position:relative; z-index:2;
  font-size:clamp(2rem,7vw,6rem); line-height:1; text-align:center; padding:0 6vw }
.ds-panel-slide[style]::after{ content:''; position:absolute; inset:0;
  background:rgba(0,0,0,.45) }

/* ------------------------------------------------------------ slider */
.ds-fx-slider{ overflow:hidden }
.ds-slider-track{ display:flex; gap:clamp(14px,2vw,26px); width:max-content;
  will-change:transform }
.ds-slide{ flex:0 0 auto; width:clamp(220px,26vw,380px) }
.ds-slide img{ width:100%; height:auto; display:block; border-radius:10px }
.ds-slide span{ display:block; margin-top:10px; font-size:.85rem; opacity:.6 }

/* ---------------------------------------------------------- sequence */
.ds-fx-sequence{ width:100%; background:var(--fx-fill); border-radius:10px;
  overflow:hidden }
.ds-fx-sequence canvas{ width:100%; height:100%; display:block; object-fit:cover }
.ds-fx-sequence.ds-ratio-16-9{ aspect-ratio:16/9 }
.ds-fx-sequence.ds-ratio-4-3{ aspect-ratio:4/3 }
.ds-fx-sequence.ds-ratio-1-1{ aspect-ratio:1/1 }
.ds-fx-sequence.ds-ratio-9-16{ aspect-ratio:9/16 }

/* -------------------------------------------------------------- skew */
.ds-fx-skew{ display:grid; gap:clamp(20px,4vw,60px) }
.ds-skew-item{ will-change:transform }
.ds-skew-item img{ width:100%; height:auto; display:block; border-radius:10px }

/* An effect that threw shows its content plainly rather than half-moved. */
.ds-fx.is-plain *{ opacity:1 !important; transform:none !important; clip-path:none !important }

@media (prefers-reduced-motion: reduce){
  .ds-fx *{ transition:none !important }
  .ds-trail{ display:none }
}

/* =====================================================================
 * The second set
 * ================================================================== */

/* ------------------------------------------------ cursor image preview */
/* No space under it by default.
 *
 * Every element carries --ds-el-gap below it, which is right for something that
 * ends where its content ends. This one ends on a rule: the last row draws its
 * bottom border, so the gap sits under a line and reads as the element having
 * been left unfinished rather than as spacing.
 *
 * A default, not a rule - Space on the element still sets a margin where one is
 * wanted, because that is written on the element itself and outranks this. */
/* The rules are this element's edges.
 *
 * Space on the element is handed to the rows instead of padding the box - see
 * the renderer - so the padding it writes onto the element itself is taken
 * back off here. Without this the figure would apply twice: once above the
 * first line and once inside every row.
 *
 * Written as its own declaration rather than a shorthand, so the left and
 * right of a Space setting still inset the element the way they do anywhere
 * else. It is only the vertical pair that belongs to the rows. */
.ds-fx-imagepreview{ position:relative; margin-bottom:0;
  padding-top:0 !important; padding-bottom:0 !important }
/* No padding out of the box.
 *
 * It was a clamp written here, which is a decision about somebody else's page
 * taken in the stylesheet: a row of one heading wants less room than a row of
 * three elements, and neither figure was reachable from the editor. A row is
 * now exactly as tall as what is in it, and the space between the words and the
 * rule is Row padding on the element.
 *
 * The typed-list rows keep the old figure - they hold a single line of words
 * with nothing to space them, so nought there is a list with no gaps at all. */
.ds-ip-row{ display:block; padding:var(--ds-ip-pad, 0); text-decoration:none; color:inherit;
  border-top:var(--ds-ip-bw, 1px) solid var(--ds-ip-bc, var(--fx-line));
  font-size:clamp(1.4rem,3.4vw,2.8rem) }
/* The typed-list rows keep the old figure - they hold a single line of words
   with no element inside to space them, so nought there is a list with no gaps
   at all. A placed row is a Box and uses the Box's own Space. */
a.ds-ip-row{ padding:clamp(14px,2vw,26px) 0 }
.ds-ip-row:last-of-type{ border-bottom:var(--ds-ip-bw, 1px) solid var(--ds-ip-bc, var(--fx-line)) }
.ds-ip-row:hover span{ opacity:.55 }
.ds-ip-row span{ transition:opacity .3s ease }

/* A row built out of elements rather than typed as a line.
 *
 * The rules above dress a link holding one line of words: a display size, and a
 * fade on the span inside it. Neither is right for a row holding a kicker, a
 * heading and a sentence - the size would set the type for all three at once,
 * and the fade would catch every span in them, including the one wrapping a
 * coloured word inside a heading.
 *
 * So a placed row keeps the rule between rows and the air around them, and
 * everything else is whatever the elements inside were set to. The row fades as
 * a whole rather than the words in it: one transition instead of one per span,
 * and it reads as the row answering rather than the text flickering. */
.ds-ip-row.ds-ip-holds{ font-size:inherit }
.ds-ip-row.ds-ip-holds:hover span{ opacity:1 }

/* ---- what a row does under the cursor ----
 *
 * Dim was written into the element with no way to turn it off. It is a taste
 * rather than a behavior, and on pale words against black it reads as the row
 * switching off rather than answering, so it is one of six now and still the
 * default - every page already using this looks exactly as it did.
 *
 * The transition is on the row whatever is chosen, so nothing snaps, and it
 * names the properties rather than using `all` - a row holding a picture would
 * otherwise transition that picture's every computed change as well. */
.ds-ip-row,
.ds-ip-row > *{ transition:opacity .3s ease, transform .3s cubic-bezier(.22,1,.36,1) }

/* The row hovered goes quieter. */
/* The contents dim, not the row.
 *
 * Opacity on the row fades its own border with it, so the rule between rows
 * faded in and out as the cursor passed - the one part of the element that
 * should stay put. The children carry it instead; they do not overlap, so
 * fading each is the same picture as fading the group. */
.ds-ip-hv-dim .ds-ip-row:hover > *{ opacity:var(--ds-ip-dim, .55) }

/* The others go quieter instead, so the eye is led by what stayed bright.
   :has on the element, so it only applies while a row is actually hovered -
   without it every row would sit dimmed until the cursor arrived. */
.ds-ip-hv-quiet:has(.ds-ip-row:hover) .ds-ip-row > *{ opacity:var(--ds-ip-dim, .4) }
.ds-ip-hv-quiet:has(.ds-ip-row:hover) .ds-ip-row:hover > *{ opacity:1 }

.ds-ip-hv-lift .ds-ip-row:hover{ transform:translateY(-6px) }
.ds-ip-hv-slide .ds-ip-row:hover{ transform:translateX(20px) }
.ds-ip-hv-grow .ds-ip-row:hover{ transform:scale(1.03) }

/* Grow and slide move the row over the rule above or below it, so the one
   being pointed at is drawn on top rather than under its neighbour. */
.ds-ip-hv-lift .ds-ip-row:hover,
.ds-ip-hv-slide .ds-ip-row:hover,
.ds-ip-hv-grow .ds-ip-row:hover{ position:relative; z-index:2 }

/* Nothing is a real answer, and reduced motion turns the moving ones into
   nothing rather than into a different movement. */
@media (prefers-reduced-motion: reduce){
  .ds-ip-row{ transition:none }
  .ds-ip-hv-lift .ds-ip-row:hover,
  .ds-ip-hv-slide .ds-ip-row:hover,
  .ds-ip-hv-grow .ds-ip-row:hover{ transform:none }
}

/* The row already carries its own air, so the first and last thing in it do
   not add more against the rule. */
.ds-ip-row.ds-ip-holds > .ds-el:first-child{ margin-top:0 }
.ds-ip-row.ds-ip-holds > .ds-el:last-child{ margin-bottom:0 }
.ds-ip-peek{ position:absolute; top:0; left:0; width:clamp(180px,22vw,320px);
  pointer-events:none; opacity:0; visibility:hidden; z-index:3; will-change:transform }
/* Two pictures in one panel, stacked.
 *
 * The first is in the flow and gives the panel its height; the second is laid
 * over it. Moving from row to row shows the new one while the old is still
 * leaving, which a single image cannot do - swapping its source is instant
 * however long the fade around it lasts. */
.ds-ip-peek img{ width:100%; height:auto; display:block; border-radius:8px }
.ds-ip-peek img + img{ position:absolute; inset:0; height:100% }

/* A row that follows its own link says so under the cursor.
 *
 * Nothing is drawn over the row and nothing is wrapped around it - the script
 * takes the click and follows the anchor the row already holds. An anchor laid
 * on top would sit between the cursor and anything in the row meant to be
 * clicked, and an anchor wrapped around it is not markup a browser keeps: it
 * closes at the first link inside and the rest of the row falls outside it. */
.ds-ip-row.ds-ip-linked{ cursor:pointer }
/* Nothing is chosen until a row is hovered, and an img with no src draws a
   broken-image icon in some browsers. */
.ds-ip-peek img:not([src]){ display:none }

/* ------------------------------------------------------- bento gallery */
.ds-fx-bento{ display:grid; grid-template-columns:repeat(6,1fr);
  grid-auto-rows:minmax(90px,12vw); gap:clamp(8px,1.2vw,16px) }
.ds-bento-cell{ margin:0; overflow:hidden; border-radius:10px; background:var(--fx-fill) }
.ds-bento-cell img{ width:100%; height:100%; object-fit:cover; display:block }
.ds-bento-cell.c1{ grid-column:span 4; grid-row:span 2 }
.ds-bento-cell.c2{ grid-column:span 2 }
.ds-bento-cell.c3{ grid-column:span 2 }
.ds-bento-cell.c4{ grid-column:span 2; grid-row:span 2 }
.ds-bento-cell.c5{ grid-column:span 2 }
.ds-bento-cell.c6{ grid-column:span 2 }
@media (max-width:700px){
  .ds-fx-bento{ grid-template-columns:repeat(2,1fr) }
  .ds-bento-cell{ grid-column:span 2 !important; grid-row:span 1 !important }
}

/* --------------------------------------------------------- radial menu */
.ds-fx-radial{ position:relative; min-height:320px; display:grid; place-items:center }
.ds-radial-btn{ position:relative; z-index:2; width:110px; height:110px; border-radius:50%;
  border:1px solid currentColor;
  background:var(--ds-radial-btn, var(--ds-bg,#0b0b0c)); color:inherit;
  cursor:pointer; font:inherit; font-size:.95rem }
.ds-radial-ring{ position:absolute; inset:0; display:grid; place-items:center;
  pointer-events:none }
.ds-fx-radial.is-open .ds-radial-ring{ pointer-events:auto }
.ds-radial-item{ position:absolute; padding:11px 20px; border-radius:999px;
  border:1px solid var(--fx-line);
  background:var(--ds-radial-arc, var(--ds-bg,#0b0b0c)); color:inherit;
  text-decoration:none; font-size:.9rem; white-space:nowrap }
.ds-radial-item:hover{ border-color:currentColor }

/* ----------------------------------------------- scroll progress links */
.ds-fx-progresslinks{ display:grid; gap:4px }
.ds-fx-progresslinks[data-side="right"]{ justify-items:end }
.ds-pl-link{ display:flex; align-items:center; gap:12px; padding:7px 0;
  text-decoration:none; color:inherit; opacity:.5; transition:opacity .3s ease }
.ds-pl-link.is-here{ opacity:1 }
.ds-pl-link i{ display:block; width:52px; height:2px;
  background:var(--ds-prog-track, var(--fx-line));
  position:relative; overflow:hidden }
.ds-pl-link i::after{ content:''; position:absolute; inset:0;
  background:var(--ds-prog-fill, currentColor);
  transform:scaleX(var(--ds-progress,0)); transform-origin:0 50% }

/* ----------------------------------------------------------- waypoints */
.ds-fx-waypoints{ display:grid; grid-template-columns:auto 1fr; gap:clamp(18px,3vw,40px) }
.ds-wp-rail{ position:relative; width:34px }
.ds-wp-line{ position:absolute; left:16px; top:0; bottom:0; width:1px;
  background:var(--ds-way-rail, var(--fx-line)) }
.ds-wp-rider{ position:absolute; left:0; top:0; width:34px; height:34px; z-index:2;
  display:grid; place-items:center }
.ds-wp-rider img{ width:100%; height:100%; object-fit:cover; border-radius:50%; display:block }
.ds-wp-dot{ width:12px; height:12px; border-radius:50%; display:block;
  background:var(--ds-way-mark, currentColor) }
.ds-wp-list{ list-style:none; margin:0; padding:0; display:grid; gap:clamp(24px,5vw,70px) }
.ds-wp-stop{ opacity:.4; transition:opacity .35s ease }
.ds-wp-stop.is-here{ opacity:1 }
.ds-wp-stop b{ display:block; font-size:.8rem; letter-spacing:.14em; opacity:.6 }
.ds-wp-stop span{ display:block; margin-top:6px; font-size:clamp(1.2rem,2.6vw,2rem) }

/* ------------------------------------------------------- looped panels */
.ds-fx-loopedpanels{ position:relative; height:74svh; min-height:400px; overflow:hidden;
  border-radius:12px; touch-action:none }
.ds-lp-slide{ position:absolute; inset:0; display:grid; place-items:center;
  background:var(--ds-bg,#0b0b0c) center/cover no-repeat }
.ds-lp-slide h3{ margin:0; position:relative; z-index:2; text-align:center; padding:0 6vw;
  font-size:clamp(1.8rem,6vw,4.6rem) }
.ds-lp-slide[style]::after{ content:''; position:absolute; inset:0; background:rgba(0,0,0,.45) }

/* --------------------------------------------------------- curved edge */
.ds-fx-curveedge{ line-height:0; color:var(--ds-bg,#0b0b0c) }
.ds-fx-curveedge svg{ width:100%; height:clamp(60px,9vw,140px); display:block }
.ds-fx-curveedge[data-edge="top"]{ transform:rotate(180deg) }

/* -------------------------------------------------------- rolling text */
.ds-fx-rollingtext{ display:grid; gap:2px }
.ds-align-center.ds-fx-rollingtext{ justify-items:center }
.ds-align-right.ds-fx-rollingtext{ justify-items:end }
.ds-roll{ display:block; text-decoration:none; color:inherit; overflow:hidden;
  height:1.1em; line-height:1.1; font-size:clamp(1.6rem,4.4vw,3.4rem) }
.ds-roll-in{ display:block; transition:transform .45s cubic-bezier(.22,1,.36,1) }
.ds-roll-in i{ display:block; font-style:normal; height:1.1em }
.ds-roll:hover .ds-roll-in,.ds-roll.is-on .ds-roll-in{ transform:translateY(-1.1em) }

/* -------------------------------------------------------- flip gallery */
.ds-fx-flipgallery{ position:relative; display:grid;
  grid-template-columns:repeat(var(--ds-cols,3),1fr); gap:clamp(8px,1.4vw,18px) }
.ds-fg-cell{ padding:0; border:0; background:none; cursor:zoom-in; line-height:0;
  border-radius:10px; overflow:hidden }
.ds-fg-cell img{ width:100%; height:100%; object-fit:cover; display:block }
.ds-fg-cell.is-open{ position:fixed; inset:6vh 8vw; z-index:9999; cursor:zoom-out;
  border-radius:12px }
.ds-fg-cell.is-open img{ object-fit:contain; background:#000 }
.ds-fg-veil{ position:fixed; inset:0; z-index:9998; background:rgba(6,6,7,.9);
  backdrop-filter:blur(10px) }
.ds-fg-veil[hidden]{ display:none }
@media (max-width:700px){ .ds-fx-flipgallery{ grid-template-columns:repeat(2,1fr) } }

/* ------------------------------------------------------- flip carousel */
.ds-fx-flipcarousel{ display:grid; gap:18px }
.ds-fc-stage{ position:relative; height:clamp(280px,42vw,520px) }
.ds-fc-item{ position:absolute; top:0; left:0; margin:0; width:62%; height:100%;
  border-radius:12px; overflow:hidden; background:var(--fx-fill) }
.ds-fc-item img{ width:100%; height:100%; object-fit:cover; display:block }
.ds-fc-item figcaption{ position:absolute; left:0; right:0; bottom:0; padding:14px 18px;
  font-size:.9rem; background:linear-gradient(transparent,rgba(0,0,0,.7)) }
.ds-fc-item.pos-0{ left:0; z-index:4 }
.ds-fc-item.pos-1{ left:26%; z-index:3; transform:scale(.93); opacity:.85 }
.ds-fc-item.pos-2{ left:44%; z-index:2; transform:scale(.86); opacity:.6 }
.ds-fc-item.pos-3{ left:56%; z-index:1; transform:scale(.8); opacity:.35 }
.ds-fc-btns{ display:flex; gap:10px }
.ds-fc-btns button{ width:46px; height:46px; border-radius:50%; cursor:pointer;
  border:1px solid var(--fx-line); background:none; color:inherit; font-size:1rem }
.ds-fc-btns button:hover{ border-color:currentColor }

/* --------------------------------------------------------- filter grid */
.ds-fgd-bar{ display:flex; flex-wrap:wrap; gap:8px; margin:0 0 clamp(16px,2.4vw,28px) }
.ds-fgd-bar button{ padding:9px 18px; border-radius:999px; cursor:pointer; font:inherit;
  font-size:.85rem; border:1px solid var(--fx-line); background:none; color:inherit;
  opacity:.6; transition:opacity .25s ease, border-color .25s ease }
.ds-fgd-bar button.on{ opacity:1; border-color:currentColor }
.ds-fgd-grid{ display:grid; grid-template-columns:repeat(var(--ds-cols,3),1fr);
  gap:clamp(10px,1.6vw,20px) }
.ds-fgd-cell{ margin:0; border-radius:10px; overflow:hidden; background:var(--fx-fill) }
.ds-fgd-cell[hidden]{ display:none }
.ds-fgd-cell img{ width:100%; height:auto; display:block; aspect-ratio:4/3; object-fit:cover }
.ds-fgd-cell figcaption{ padding:12px 14px; font-size:.85rem; opacity:.7 }
@media (max-width:700px){ .ds-fgd-grid{ grid-template-columns:repeat(2,1fr) } }

/* ------------------------------------------------- directional marquee */
.ds-fx-dirmarquee{ overflow:hidden; padding:clamp(10px,1.6vw,20px) 0;
  border-block:1px solid var(--fx-line) }
.ds-dm-track{ display:flex; width:max-content; will-change:transform }
.ds-dm-item{ padding:0 clamp(18px,2.6vw,40px); white-space:nowrap;
  font-size:clamp(1.4rem,4vw,3.4rem); line-height:1.1 }

/* --------------------------------------------------------- curve swipe */
.ds-fx-curveswipe{ position:relative; overflow:hidden; border-radius:12px;
  min-height:clamp(320px,46vw,600px); display:grid }
.ds-cs-stage{ position:relative; grid-area:1/1 }
.ds-cs-panel{ position:absolute; inset:0; display:grid; place-items:center;
  background:var(--fx-fill) }
.ds-cs-panel h3{ margin:0; font-size:clamp(1.8rem,6vw,4.4rem); text-align:center; padding:0 6vw }
.ds-cs-veil{ grid-area:1/1; width:100%; height:100%; position:relative; z-index:3;
  color:var(--ds-bg,#0b0b0c); pointer-events:none }
/* Top, not bottom. The stage is up to 600px tall, so a control pinned to the
   bottom sits a long way from the text that explains what it does. */
.ds-cs-btn{ position:absolute; right:20px; top:20px; z-index:4; padding:12px 26px;
  border-radius:999px; border:1px solid currentColor; background:var(--ds-bg,#0b0b0c);
  color:inherit; cursor:pointer; font:inherit; font-size:.9rem }

/* ----------------------------------------------------- scroll waypoints */
.ds-fx-waypointpath{ position:relative }
.ds-wpp-svg{ width:100%; height:auto; display:block; color:currentColor }
.ds-wpp-rider{ position:absolute; top:0; left:0; width:44px; height:44px;
  display:grid; place-items:center }
.ds-wpp-rider img{ width:100%; height:100%; object-fit:cover; border-radius:50%; display:block }
.ds-wpp-stops{ display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:18px; margin-top:clamp(16px,3vw,34px) }
.ds-wpp-stop{ opacity:.4; transition:opacity .35s ease }
.ds-wpp-stop.is-here{ opacity:1 }
.ds-wpp-stop b{ display:block; font-size:.78rem; letter-spacing:.14em; opacity:.6 }
.ds-wpp-stop span{ display:block; margin-top:5px; font-size:1.05rem }


/* --------------------------------------------------------- full width text */
/* The Full width text row in Typography, same as the footer block reads.
 *
 * The row was written for one of the three things on the site that draw a line
 * of type across a whole column, and only that one was reading it - so setting
 * the full width font changed the footer and left the element and the words
 * behind an image trail on whatever they had. One row, everything it names.
 *
 * The element's own Weight and Tracking are more specific than this, so a page
 * that sets them still wins - this is what it falls back to. */
.ds-fx-fittext{ width:100% }
/* Clipped sideways only.
 *
 * overflow:hidden clips both ways, and a line box set under 1em is shorter
 * than the letters in it - so the tops of a display line were sliced flat. The
 * clipping is here to stop a line overflowing its column before the size has
 * been worked out, which is a horizontal problem; clip on one axis leaves the
 * other visible, where hidden would force it to scroll. */
/* On its own layer.
 *
 * A line this size sits inside a smooth scroller, which moves the whole page by
 * fractions of a pixel - so the glyphs were re-rasterized at a slightly
 * different sub-pixel offset on every frame, and 265px letterforms redrawn
 * sixty times a second shimmer. Promoted, it is drawn once and moved, which is
 * what a compositor is for. */
.ds-fit{ margin:0; width:100%; white-space:nowrap;
  overflow-x:clip; overflow-y:visible;
  transform:translateZ(0); will-change:transform; backface-visibility:hidden;
  font-family:var(--ds-font-fullwidth, var(--ds-font-heading));
  font-weight:var(--fit-weight, var(--ds-fullwidth-weight,500));
  text-transform:var(--ds-fullwidth-case,none);
  letter-spacing:var(--ds-fullwidth-track,-0.02em);
  line-height:var(--ds-fullwidth-lh,.9) }
.ds-fit [data-fittext]{ display:inline-block; white-space:nowrap }
.ds-fx-fittext[data-tight="tight"] .ds-fit{ letter-spacing:-.03em }
.ds-fx-fittext[data-tight="wide"] .ds-fit{ letter-spacing:.06em }
.ds-fx-fittext[data-weight="300"] .ds-fit{ font-weight:300 }
.ds-fx-fittext[data-weight="400"] .ds-fit{ font-weight:400 }
.ds-fx-fittext[data-weight="600"] .ds-fit{ font-weight:600 }
.ds-fx-fittext[data-weight="700"] .ds-fit{ font-weight:700 }
.ds-fx-fittext[data-weight="800"] .ds-fit{ font-weight:800 }

/* ------------------------------------------------------ shaded pictures */
.ds-shaded{ position:relative; display:block; line-height:0 }
.ds-shaded img{ width:100%; height:auto; display:block }
.ds-shaded canvas{ position:absolute; inset:0; width:100%; height:100%;
  display:block; opacity:0; transition:opacity .3s ease }

/* The canvas takes over only once it has the picture on it. Until then, and
   for ever on a browser without WebGL, the img is what is seen. */
.ds-shaded.is-shaded canvas{ opacity:1 }
.ds-shaded.is-shaded img{ visibility:hidden }

/* ---------------------------------------------------------------------
 * A marquee driven by the scroll
 *
 * Four copies on one line, moved a quarter of their width by the page. The
 * frame clips, so what leaves one side is already arriving at the other and
 * the line never shows an end.
 * ------------------------------------------------------------------ */
.ds-fx-marqueescroll{ overflow:hidden; width:100% }

.ds-fx-marqueescroll .ds-marquee-track{
  display:flex;
  align-items:center;
  width:max-content;
  will-change:transform;
}

.ds-fx-marqueescroll .ds-marquee-word{
  white-space:nowrap;
  font-size:var(--ds-marquee-size, clamp(2.5rem, 9vw, 8rem));
  line-height:1;
}

/* Between one copy and the next. Blank leaves a plain gap, which is what a
   marquee with nothing set looks like. */
.ds-fx-marqueescroll .ds-marquee-sep{
  padding:0 var(--ds-marquee-gap, .35em);
  opacity:.55;
}

.ds-fx-marqueescroll .ds-marquee-word + .ds-marquee-word{
  padding-left:var(--ds-marquee-gap, .35em);
}
