/**
 * 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 }

/* No shadow of its own.
 *
 * A shadow is drawn from the element's box, so on a cut-out picture it
 * fills the see-through part with a rounded rectangle - the transparency
 * reads as a pale block sitting behind the subject. Every product shot
 * dropped by this element is a cut-out, so the shadow was wrong for the
 * one thing it is mostly used for.
 *
 * It is not simply deleted: .ds-it-shot is named in the Images target in
 * DS_Theme::target_map(), so ticking Images under Box shadows puts the
 * theme's own shadow on these and unticking it takes the shadow off.
 * Written here it could not be reached from the screen at all. */
.ds-it-shot{ position:absolute; top:0; left:0; display:block;
  pointer-events:none; will-change:transform; border-radius:8px }

/* On where the element's own Shadow field says On.
 *
 * ds-el-shadow is written by the builder for every element carrying the
 * standard Shadow field - Theme default / Off / On - so this needs no
 * field of its own, and the control sits where it does on every other
 * element rather than somewhere particular to this one.
 *
 * Off is already handled: .ds-el-noshadow img is in the builder's own
 * stylesheet and these are img elements. */
.ds-el-shadow .ds-it-shot{
  box-shadow:var(--ds-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 */
/* Two gaps, because there are two kinds of gap here.
 *
 * One figure is separated from the next by a generous amount - they are separate
 * statements and want air between them. But this was written as a single value,
 * and a single value is a row gap as well as a column gap: the moment a counter
 * wrapped onto a second line, the space meant to sit between two figures landed
 * between a figure and its own caption. At a 1098px window that was 55px of
 * nothing above the words, inside a tile that then had to be tall enough to hold
 * it - which reads as the element having been given a large padding it does not
 * have. The row gap is small, because a caption belongs to the figure above it. */
.ds-fx-counter{ display:flex; flex-wrap:wrap; gap:10px 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; line-height:1;
  font-variant-numeric:tabular-nums; font-weight:500;
  color:var(--ds-counter-num, inherit) }
/* The clamp only where no size has been chosen.
 *
 * It was on .ds-count b, an element selector, which beats the .ds-as-* class the
 * counter's size selector puts on that same element - so picking H1 landed the
 * right class against the right token and the figure went on drawing at whatever
 * the clamp said. Every size in the dropdown did nothing, and the dropdown was
 * not the thing that was wrong. */
.ds-count > b:not([class*="ds-as-"]){ font-size:clamp(2.6rem,7vw,5.5rem) }
.ds-count > span{ display:block; margin-top:8px; letter-spacing:.08em;
  text-transform:uppercase;
  /* Its own color where one is set, and the old faded ink where it is not -
     an opacity and a color together would tint whatever was chosen. */
  color:var(--ds-counter-label, inherit);
  opacity:var(--ds-counter-label-fade, .6) }
/* And the label's size, for the same reason as the figure's above. */
/* The label, which is a direct child. Unscoped, this reached every span
   inside the counter - including the ticker's own digit columns, which are
   spans nested in the figure. They were being drawn at .9rem inside an 80px
   number, so each masked window was 14px tall and the digits it was meant to
   show were six times its height. */
.ds-count > span:not([class*="ds-as-"]){ font-size:.9rem }

/* ------------------------------------------------------ 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 }
/* The line height belongs to the heading too, for the same reason the size
   above it does. Hardcoded at 1.06 this was the one heading on the site
   that ignored Theme Settings: every other h1 took its line height from
   the type screen and the hero - the largest type on the page and the
   first thing anybody sees - sat at 1.06 whatever was set, with its
   descenders running into the paragraph beneath. */
h1.ds-fx-text,h2.ds-fx-text,h3.ds-fx-text{
  font-size:clamp(1.8rem,5vw,4.4rem) }
h1.ds-fx-text{ line-height:var(--ds-h1-lh,1.2em) }
h2.ds-fx-text{ line-height:var(--ds-h2-lh,1.2em) }
h3.ds-fx-text{ line-height:var(--ds-h3-lh,1.25em) }
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 gray 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 traveling 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 }
/* Scoped to the track these belong to.
   Bare, .ds-slide claimed every slide on the page, and the row slideshow uses
   the same class for a slide that is meant to fill the screen. effects.css
   loads after builder.css, so a full width scroll driven slideshow drew each
   of its slides 380px wide in the corner of the window with the photograph
   cropped to match - on every page, whether a slider element was on it or
   not. The img and span rules went the same way: they restyled the picture
   and the caption of a slideshow that has neither. */
.ds-slider-track > .ds-slide{ flex:0 0 auto; width:clamp(220px,26vw,380px) }
.ds-slider-track > .ds-slide img{ width:100%; height:auto; display:block; border-radius:10px }
.ds-slider-track > .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
 * colored 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 neighbor. */
.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);
}

/* ---- the tickerboard ----
 *
 * One column per place, each exactly one digit tall with the digits stacked
 * inside it. The column is the mask; the strip is what moves. Sliding a strip
 * behind a window is what makes this a board turning rather than text being
 * swapped out, which is all it was before.
 *
 * The offset is carried in em, so a figure drawn at H1 on a desktop and H3 on a
 * phone keeps its digits registered without anything measuring pixels. */
.ds-tick{ display:inline-flex; align-items:baseline }
.ds-tick-col{
  display:inline-block;
  height:1em;
  overflow:hidden;
  /* The digits are the width of a figure, and tabular figures keep every place
     the same width so the number does not breathe as it turns. */
  font-variant-numeric:tabular-nums;
  vertical-align:baseline;
}
.ds-tick-strip{
  display:block;
  will-change:transform;
  transform:translateY(calc(var(--ds-tick-at, 0) * -1em));
}
.ds-tick-strip > span{ display:block; height:1em; line-height:1 }
/* A point or a minus sits on the line with them rather than in a window. */
.ds-tick-fixed{ display:inline-block; font-variant-numeric:tabular-nums }

/* Asked for less movement means the figure, not the journey. */
@media (prefers-reduced-motion: reduce){
  .ds-tick-strip{ transition:none }
}

/* ----------------------------------------------------- fill from left
 *
 * The words are painted with a gradient clipped to the glyphs - the same
 * technique the heading gradient uses - and what the scroll moves is where that
 * gradient sits, not the words themselves.
 *
 * Two stops at the same position, so the join is an edge sweeping across the
 * line rather than a soft smear. The sheet is twice the width of the line, so
 * sliding it from one end to the other carries that edge the whole way.
 *
 * The color is read off the element and written in as --ds-tfx-ink, so the
 * effect still never picks one: it works on any heading in any palette and
 * follows a color set anywhere above it.
 *
 * It cannot be currentColor, and that is why this never worked. The gradient was
 * built from currentColor and this rule sets color:transparent three lines
 * below - in the same rule. currentColor resolves against the element's own
 * computed color, so it resolved against the transparency just declared here
 * and every stop came out rgba(0,0,0,0): a transparent gradient, clipped to
 * text, with the text made transparent to show it through. Nothing on screen,
 * on any element, in any state.
 *
 * A color cannot be captured before it is overwritten from inside the rule that
 * overwrites it, so text-fx.js reads the computed color and writes it here as a
 * literal before adding the class. currentColor stays as the fallback so a run
 * that somehow arrives without it degrades to the old behavior rather than
 * disappearing. */
.ds-tfx-fill{
  --ds-tfx-dim: .33;
  background-image: linear-gradient(
    90deg,
    var(--ds-tfx-ink, currentColor) 0 50%,
    color-mix(in srgb, var(--ds-tfx-ink, currentColor) calc(var(--ds-tfx-dim, .33) * 100%), transparent) 50% 100%
  );
  background-size: 200% 100%;
  background-position-x: 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Nothing to clip to on a browser that cannot, so the words keep their color
   and simply do not fill. Better a plain heading than an invisible one. */
@supports not ((background-clip: text) or (-webkit-background-clip: text)){
  .ds-tfx-fill{ color: currentColor; -webkit-text-fill-color: currentColor; background-image: none }
}

/* Somebody who asked for less movement gets the finished line, not a third of
   one sitting there waiting for a scroll that will never animate it. */
@media (prefers-reduced-motion: reduce){
  .ds-tfx-fill{ background-position-x: 0% }
}

/* ---------------------------------------------------------------------------
 * Chart
 *
 * The shape and the key, side by side or stacked. Everything the chart needs
 * to be read is in the markup; this only places it and draws it in.
 * ------------------------------------------------------------------------ */
.ds-fx-chart{ display:flex; align-items:center; gap:clamp(16px,3vw,40px); flex-wrap:wrap }
.ds-align-center.ds-fx-chart{ justify-content:center }
.ds-align-right.ds-fx-chart{ justify-content:flex-end }

/* Underneath rather than beside: the plot takes the full width it is given and
   the key runs under it, which is the only arrangement that survives a phone. */
.ds-chart-key-below.ds-fx-chart{ flex-direction:column; align-items:flex-start }
.ds-align-center.ds-chart-key-below.ds-fx-chart{ align-items:center }

/* Under the chart, the key belongs to it and sits close.
 *
 * The 40px that separates a chart from a key beside it is a column gutter; the
 * same figure underneath reads as a gap between two unrelated things, and the
 * key stopped looking like the chart's own labels. */
.ds-chart-key-below.ds-fx-chart{ gap:14px }

.ds-chart-plot{ flex:0 0 auto; width:var(--ds-chart-size,260px); max-width:100% }
.ds-chart-svg{ display:block; width:100%; height:auto; overflow:visible }


.ds-chart-web{ fill:none; stroke:currentColor; stroke-width:.4; opacity:.18 }

.ds-chart-key{ display:flex; flex-direction:column; gap:8px; min-width:0 }
.ds-chart-key-below .ds-chart-key{ flex-direction:row; flex-wrap:wrap; gap:8px 20px }

.ds-chart-item{ display:flex; align-items:baseline; gap:6px; min-width:0 }
.ds-chart-dab{ flex:0 0 auto; width:10px; height:10px; border-radius:2px; transform:translateY(1px) }
.ds-chart-label{ min-width:0; overflow-wrap:anywhere }
/* The figure sits with the words it belongs to.
 *
 * margin-left:auto pushed it to the far edge of the key, so a short label left
 * a hand's width of nothing between the name and its number and the two read as
 * separate columns rather than as one fact. */
.ds-chart-num{ font-variant-numeric:tabular-nums; opacity:.7 }

/* Drawn in.
 *
 * The shape is already correct in the markup; these only hide it so the script
 * can reveal it. Without script, or with motion turned down, everything below
 * is skipped and the chart is simply there - which is the state it has to be
 * readable in anyway. */
.ds-fx-chart[data-anim="yes"] .ds-chart-arc{ stroke-dasharray:0 9999 }
.ds-fx-chart[data-anim="yes"] .ds-chart-cols .ds-chart-bar{ transform:scaleY(0) }
/* The line is wiped in from its left rather than drawn by a dash offset.
   A dash length is measured in the drawing's own units, and this drawing is
   stretched to the column while its stroke is held off that stretch - so the
   two disagreed and the line came in as a row of gaps. A wipe is the same
   movement and cannot disagree with anything. */
.ds-fx-chart.ds-chart-line[data-anim="yes"] .ds-chart-svg{ clip-path:inset(0 100% 0 0) }
.ds-fx-chart[data-anim="yes"] .ds-chart-dot,
.ds-fx-chart[data-anim="yes"] .ds-chart-shape{ opacity:0 }

@media (prefers-reduced-motion: reduce){
  .ds-fx-chart[data-anim="yes"] .ds-chart-arc{ stroke-dasharray:inherit }
  .ds-fx-chart[data-anim="yes"] .ds-chart-cols .ds-chart-bar{ transform:none }
  .ds-fx-chart.ds-chart-line[data-anim="yes"] .ds-chart-svg{ clip-path:none }
  .ds-fx-chart[data-anim="yes"] .ds-chart-dot,
  .ds-fx-chart[data-anim="yes"] .ds-chart-shape{ opacity:1 }
}

/* Bars as columns.
 *
 * One column per figure: the bar in a well that gives every column the same
 * height to grow in, and the words underneath. Equal fractions, so a bar is
 * exactly as wide as the text it describes and sits directly over it. */
.ds-chart-bar.ds-fx-chart .ds-chart-plot{ width:min(100%, calc(var(--ds-chart-size,260px) * 2)) }

.ds-chart-cols{
  display:grid;
  grid-auto-flow:column;
  grid-auto-columns:1fr;
  gap:clamp(8px,2vw,20px);
  align-items:end;
}

/* The well is the full height every bar is measured against, so two charts
   with different figures still stand on the same baseline. */
.ds-chart-well{ display:flex; align-items:flex-end; height:var(--ds-chart-size,260px) }
.ds-chart-cols .ds-chart-bar{ width:100%; transform-origin:50% 100% }

.ds-chart-tick{
  display:flex;
  align-items:baseline;
  gap:6px;
  flex-wrap:wrap;
  margin-top:10px;
}


/* The tooltip.
 *
 * One per chart, moved to whatever is being pointed at, rather than one node
 * per slice sitting in the markup waiting. It is positioned against the chart
 * itself so it cannot be clipped by a row's overflow. */
.ds-fx-chart{ position:relative }

.ds-chart-tip{
  position:absolute;
  z-index:6;
  pointer-events:none;
  opacity:0;
  transform:translate(-50%,-100%);
  margin-top:-10px;
  padding:6px 10px;
  border-radius:4px;
  background:var(--ds-heading,#111);
  color:var(--ds-bg,#fff);
  font-size:13px;
  line-height:1.3;
  white-space:nowrap;
  transition:opacity .12s ease;
}

.ds-chart-tip.is-on{ opacity:1 }

/* The targets. Invisible, but they have to be reachable. */
.ds-chart-hit{ fill:transparent; cursor:pointer }
.ds-chart-arc,
.ds-chart-cols .ds-chart-bar{ cursor:pointer }

/* Pointed at, or reached by keyboard: the rest of the chart steps back so the
   one being read is obvious. */
.ds-fx-chart[data-tips="yes"] .ds-chart-arc:hover,
.ds-fx-chart[data-tips="yes"] .ds-chart-arc:focus-visible,
.ds-fx-chart[data-tips="yes"] .ds-chart-cols .ds-chart-bar:hover,
.ds-fx-chart[data-tips="yes"] .ds-chart-cols .ds-chart-bar:focus-visible{ filter:brightness(1.15) }

.ds-chart-hit:focus-visible{ outline:2px solid var(--ds-accent); outline-offset:2px }

/* ------------------------------------------------------------------
   A line, and a radar
   ------------------------------------------------------------------
   Both draw a shape in SVG and put their words over the top in HTML.
   Text inside the drawing is scaled by the viewBox, so it never matched
   the page it sat on; over the top it is the page's own type and can
   wrap rather than clip.
   ------------------------------------------------------------------ */
.ds-chart-graph{ position:relative }

/* A line is a figure over time and wants the width it is given. Only its
   height comes from the size setting; the drawing stretches to the column
   and the stroke is kept off the stretch. */
.ds-chart-line.ds-fx-chart .ds-chart-plot{ width:100%; flex:1 1 100% }
.ds-chart-line .ds-chart-graph{ height:var(--ds-chart-size,260px) }
.ds-chart-line .ds-chart-svg{ height:100% }

/* The dot sits on its own vertex and stays round however wide the chart is. */
.ds-chart-line .ds-chart-dot{
  position:absolute;
  width:10px;
  height:10px;
  border-radius:50%;
  transform:translate(-50%,-50%);
  cursor:pointer;
}
.ds-chart-line .ds-chart-dot:focus-visible{ outline:2px solid var(--ds-accent); outline-offset:3px }

/* The names under the points they belong to, each pulled to its own point
   rather than spaced evenly on a row of their own. */
.ds-chart-axis{ position:relative; height:1.6em; margin-top:10px }
.ds-chart-axis .ds-chart-tick{
  position:absolute;
  top:0;
  display:block;
  margin-top:0;
  white-space:nowrap;
  opacity:.75;
}

/* A radar is square, and its names are allowed outside that square.
 *
 * The room for them is a share of the box rather than a fixed measure, so a
 * chart in a narrow column shrinks its drawing and keeps its names instead of
 * standing at full size and hanging out of the column. The square never grows
 * past the size that was asked for. */
.ds-chart-radar.ds-fx-chart .ds-chart-plot{
  width:100%;
  max-width:calc(var(--ds-chart-size,260px) * 2);
  padding:1.4em 0;
}
/* Half the box is the drawing and a quarter each side is for the names, at
   every width - so the room a name gets shrinks with the chart rather than
   staying put while the column narrows around it. */
.ds-chart-radar .ds-chart-graph{
  width:min(var(--ds-chart-size,260px), 50%);
  aspect-ratio:1;
  margin-inline:auto;
}

/* A name wraps rather than hangs off the chart. The room each side is half
   the square, so anything longer than that turns onto a second line instead
   of pushing the column open. */
.ds-chart-point{
  position:absolute;
  font-size:inherit;
  line-height:1.2;
  max-width:46%;
  text-align:center;
  opacity:.75;
}

/* ------------------------------------------------------------------
   Progress bars
   ------------------------------------------------------------------
   The label and the figure sit above the bar, not inside it. A figure
   printed on the bar is unreadable exactly when the bar is shortest,
   which is when a low number most needs reading.
   ------------------------------------------------------------------ */
.ds-fx-meter{ display:grid; gap:clamp(12px,2vw,20px) }
.ds-meter-row{ display:grid; gap:6px }
.ds-meter-said{ display:flex; align-items:baseline; gap:10px }
.ds-meter-label{ font-weight:600 }
.ds-meter-num{ margin-left:auto; font-variant-numeric:tabular-nums; opacity:.7 }
.ds-meter-track{
  position:relative;
  overflow:hidden;
  background:color-mix(in srgb, currentColor 12%, transparent);
}
.ds-meter-fill{
  height:100%;
  border-radius:inherit;
  background:var(--ds-accent, currentColor);
  transform-origin:left center;
}

/* Nothing has run yet: the bar is already at its figure, so a failed or
   absent script leaves a readable chart rather than an empty track. */
.ds-fx-meter[data-anim="yes"].is-armed .ds-meter-fill{ transform:scaleX(0) }

@media (prefers-reduced-motion:reduce){
  .ds-fx-meter[data-anim="yes"].is-armed .ds-meter-fill{ transform:none }
}

/* ------------------------------------------------------------------
   Social links
   ------------------------------------------------------------------
   The list itself is drawn by DS_Theme and dressed by the theme sheet,
   which already reads --ds-social-ink, --ds-social-bg, --ds-social-hit,
   --ds-social-radius and --ds-social-size. Nothing here restyles a
   mark; the element only sets those variables and places the row.
   ------------------------------------------------------------------ */
.ds-fx-social{ display:flex }
.ds-align-center.ds-fx-social{ justify-content:center }
.ds-align-right.ds-fx-social{ justify-content:flex-end }

.ds-fx-social .ds-footer-social{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  list-style:none;
  margin:0;
  padding:0;
}

/* Icon only: no hit box behind the mark, so a row of marks sits at the
   size that was asked for rather than inside a 38px circle of nothing. */
.ds-social-plain{ --ds-social-hit:auto; --ds-social-radius:0 }
.ds-social-plain .ds-footer-social a{ width:auto; height:auto }

/* In a shape: the mark keeps its size and the shape grows around it. */
.ds-social-round,
.ds-social-square{
  --ds-social-hit:calc(var(--ds-social-size,24px) * 1.9);
  --ds-social-bg:color-mix(in srgb, currentColor 10%, transparent);
}
.ds-social-round{ --ds-social-radius:50% }
.ds-social-square{ --ds-social-radius:6px }

/* ------------------------------------------------------------------
   Pricing table
   ------------------------------------------------------------------
   One plan. Three of them are three columns, so the card fills the
   height it is given and the button is pushed to the bottom - a row of
   plans with different numbers of features still has its buttons on
   one line.
   ------------------------------------------------------------------ */
.ds-fx-pricing{
  display:flex;
  flex-direction:column;
  gap:14px;
  position:relative;
}

/* Padding belongs to the card, not to the plan. Given a background or an edge
   it becomes a card and takes the inset that goes with one; left plain it
   stands on the same left edge as every other element in the column. */
.ds-fx-pricing.is-card{ --ds-price-pad:clamp(20px,3vw,34px); padding:var(--ds-price-pad) }
.ds-fx-pricing.is-card .ds-flag-right{ right:clamp(20px,3vw,34px) }
.ds-fx-pricing.is-card .ds-flag-left{ left:clamp(20px,3vw,34px) }

/* A plan fills its column only when it is what the column is for.
 *
 * Three plans in three columns are the same height and their buttons land on
 * one line, which is the whole point of a row of them. Stacked in with other
 * elements it takes the height it needs - filling there made one card as tall
 * as everything beside it put together. */
.ds-fx-pricing:only-child{ height:100% }
.ds-align-center.ds-fx-pricing{ text-align:center; align-items:center }
.ds-align-right.ds-fx-pricing{ text-align:right; align-items:flex-end }

/* The highlight label sits on the card's top edge, at whichever end it is
   asked to. Centered it needs the transform on both axes, so the horizontal
   offset is written per position rather than shared. */
.ds-price-flag{
  position:absolute;
  top:0;
  margin:0;
  padding:4px 12px;
  border-radius:999px;
  background:var(--ds-accent);
  color:var(--ds-bg,#fff);
  font-size:.8em;
  font-weight:600;
}
.ds-flag-right{ right:0; transform:translateY(-50%) }
.ds-flag-left{ left:0; transform:translateY(-50%) }
.ds-flag-center{ left:50%; transform:translate(-50%,-50%) }

.ds-price-name{ margin:0 }

/* The figure, with whatever stands in front of and behind it.
 *
 * A row of inline pieces, so a mark beside the price can line up against a
 * number several times its height. Where a mark is asked to sit above or
 * below instead, it is pushed onto its own line by taking the full width. */
.ds-price-fig{
  display:flex;
  align-items:baseline;
  flex-wrap:wrap;
  column-gap:8px;
  margin:0;
}
.ds-align-center.ds-fx-pricing .ds-price-fig{ justify-content:center }
.ds-align-right.ds-fx-pricing .ds-price-fig{ justify-content:flex-end }

/* The default is only the default. A price asked to be H2 takes the theme's
   H2, so the clamp below has to stand aside rather than out-weigh it. */
.ds-price-num{ line-height:1 }
.ds-price-num:not([class*="ds-as-"]){ font-size:clamp(2rem,5vw,3.4rem); font-weight:700 }
.ds-price-prefix,
.ds-price-suffix{ opacity:.7; line-height:1 }

/* Where a mark sits against the figure. A flex item aligns itself, so each
   mark answers for its own position rather than the row answering for both. */
.ds-price-v-top{ align-self:flex-start }
.ds-price-v-middle{ align-self:center }
.ds-price-v-bottom{ align-self:flex-end }

/* On its own line above or below. Order puts it there whichever side of the
   figure it was written on, so a suffix asked to sit above does not have to
   be typed into the prefix field to get there. */
.ds-price-at-top,
.ds-price-at-bottom{ flex:0 0 100%; align-self:auto }
.ds-price-at-top{ order:-1 }
.ds-price-at-bottom{ order:1 }
.ds-align-center.ds-fx-pricing .ds-price-at-top,
.ds-align-center.ds-fx-pricing .ds-price-at-bottom{ text-align:center }
.ds-align-right.ds-fx-pricing .ds-price-at-top,
.ds-align-right.ds-fx-pricing .ds-price-at-bottom{ text-align:right }

.ds-price-note{ margin:0; opacity:.75 }

/* Full width of the card whatever the card's alignment, so banded rows are
   bands across it rather than a stripe the width of the longest feature. */
.ds-price-list{ list-style:none; margin:0; padding:0; display:grid; align-self:stretch; width:100% }
.ds-price-list li{ position:relative; padding-left:1.5em }
.ds-align-center.ds-fx-pricing .ds-price-list li{ padding-left:0 }
.ds-price-list li::before{
  content:"";
  position:absolute;
  left:.25em;
  top:.5em;
  width:.5em;
  height:.5em;
  border-radius:50%;
  background:var(--ds-price-mark, var(--ds-accent));
}
.ds-align-center.ds-fx-pricing .ds-price-list li::before{ display:none }

/* Plain rows are spaced apart; banded rows are not, because a gap between
   two bands is a stripe of the page showing through the middle of them. */
.ds-price-list:not(.is-striped){ gap:8px }
/* Bands run the full width of the card, through its padding and out to its
   edges - inside the padding they read as a stripe the width of the text
   rather than as a band across the plan. The text keeps the inset the rest
   of the card stands on, so only the color reaches the edge. */
.ds-fx-pricing.is-card .ds-price-list.is-striped{
  width:calc(100% + (var(--ds-price-pad, 0px) * 2));
  margin-inline:calc(var(--ds-price-pad, 0px) * -1);
}
.ds-price-list.is-striped li{
  padding:.5em .75em .5em 1.75em;
  background:var(--ds-price-row, transparent);
}
.ds-fx-pricing.is-card .ds-price-list.is-striped li{
  padding-left:calc(var(--ds-price-pad, 0px) + 1.25em);
  padding-right:var(--ds-price-pad, 0px);
}
.ds-fx-pricing.is-card .ds-price-list.is-striped li::before{
  left:calc(var(--ds-price-pad, 0px) + .25em);
}
.ds-price-list.is-striped li:nth-child(even){
  background:var(--ds-price-row2, color-mix(in srgb, currentColor 7%, transparent));
}
.ds-price-list.is-striped li::before{ left:.75em; top:1em }
.ds-align-center.ds-fx-pricing .ds-price-list.is-striped li{ padding-left:.75em }
.ds-align-center.ds-fx-pricing.is-card .ds-price-list.is-striped li{ padding-left:var(--ds-price-pad, 0px) }

/* Pushed to the bottom of whatever height the card is given, so a row of
   plans with different numbers of features has its buttons on one line. */
.ds-price-act{ margin-top:auto; display:flex }
.ds-align-center.ds-fx-pricing .ds-price-act{ justify-content:center }
.ds-align-right.ds-fx-pricing .ds-price-act{ justify-content:flex-end }

/* ------------------------------------------------------------------
   Team member
   ------------------------------------------------------------------ */
.ds-fx-person{ display:flex; flex-direction:column; gap:12px }
.ds-align-center.ds-fx-person{ text-align:center; align-items:center }
.ds-align-right.ds-fx-person{ text-align:right; align-items:flex-end }

.ds-person-shot{ width:100%; overflow:hidden; line-height:0; position:relative }
.ds-person-shot img{ width:100%; height:100%; object-fit:cover; display:block;
  transition:filter .45s ease }

/* The photo moves inside its own frame, which is what the frame's clipping is
   for - the picture grows and the space it occupies on the page does not. */
.ds-person-over,
.ds-person-under{ position:absolute; inset:0; line-height:0 }
.ds-person-over{ position:relative; z-index:1 }
.ds-person-over img,
.ds-person-under img{ width:100%; height:100%; object-fit:cover; display:block }

.ds-person-over{ transition:transform 1.6s cubic-bezier(.2,.7,.3,1), opacity .45s ease }
.ds-shot-fast .ds-person-over{ transition-duration:.45s, .45s }

.ds-shot-slow:hover .ds-person-over,
.ds-shot-fast:hover .ds-person-over{ transform:scale(1.08) }

/* Out is the other way round: it starts in and settles back, which suits a
   portrait that is already tight on the face. */
.ds-shot-out .ds-person-over{ transform:scale(1.08) }
.ds-shot-out:hover .ds-person-over{ transform:scale(1) }

/* Swap. The second photo is already on the page underneath, so the first
   simply stops covering it. */
.ds-shot-swap:hover .ds-person-over{ opacity:0 }

/* Grayscale, always or until pointed at. */
.ds-shot-gray-yes img{ filter:grayscale(1) }
.ds-shot-gray-hover img{ filter:grayscale(1) }
.ds-shot-gray-hover:hover img{ filter:grayscale(0) }

/* No hover on a touch screen, so nothing that only happens on hover is the
   only way to see something. The swap's second photo is shown instead. */
@media (hover:none){
  .ds-shot-swap .ds-person-over{ opacity:1 }
  .ds-shot-gray-hover img{ filter:grayscale(0) }
}

@media (prefers-reduced-motion:reduce){
  .ds-person-over{ transition:opacity .45s ease }
  .ds-shot-slow:hover .ds-person-over,
  .ds-shot-fast:hover .ds-person-over,
  .ds-shot-out .ds-person-over{ transform:none }
}
.ds-person-square .ds-person-shot{ aspect-ratio:1 }
.ds-person-round .ds-person-shot{ aspect-ratio:1; border-radius:50%; max-width:280px }
.ds-person-tall .ds-person-shot{ aspect-ratio:3/4 }
.ds-person-wide .ds-person-shot{ aspect-ratio:4/3 }

.ds-person-name{ margin:0 }
.ds-person-role{ margin:0; color:var(--ds-accent) }
.ds-person-bio{ opacity:.85 }
.ds-person-bio > :last-child{ margin-bottom:0 }
.ds-person-links{ display:flex; gap:10px; list-style:none; margin:0; padding:0 }

/* ------------------------------------------------------------------
   Timeline
   ------------------------------------------------------------------
   A rule down the entries with a marker on each. The rule is drawn on
   the list rather than per row, so it runs unbroken between markers
   whatever height a row turns out to be.
   ------------------------------------------------------------------ */
.ds-time-list{ list-style:none; margin:0; padding:0; position:relative }
.ds-time-row{ position:relative; padding:0 0 clamp(20px,3vw,34px) 34px }
.ds-time-row:last-child{ padding-bottom:0 }

/* The rule is each row's own left edge, so it stops with the last marker
   instead of running past it into empty space. */
/* Each segment runs from its own marker to the next one, overlapping the gap
   between rows so the line is unbroken - and the last row draws none, so the
   line stops at the last marker rather than running on into the space under
   it. */
.ds-time-row::before{
  content:"";
  position:absolute;
  left:5px;
  /* Half the date's line box - the point the marker is centered on. */
  top:.68em;
  bottom:calc(-.68em - 1px);
  width:2px;
  background:color-mix(in srgb, currentColor 18%, transparent);
}
.ds-time-row:last-child::before{ display:none }

/* Centered on the line it hangs off, and reaching back out to the rule.
   34px is the row's own indent, so the dot lands on the rule whatever the
   line above it is doing. */
.ds-time-when,
.ds-time-title{ position:relative }

.ds-time-dot{
  position:absolute;
  left:-34px;
  top:50%;
  transform:translateY(-50%);
  width:12px;
  height:12px;
  border-radius:50%;
  background:var(--ds-time-ink, var(--ds-accent));
}

/* An explicit line height so the rule below can start exactly where the
   marker sits. Inherited, the line box is whatever the theme's body copy
   uses and the two drift apart on any site that sets it differently. */
.ds-time-when{ margin:0; font-size:.85em; line-height:1.6; opacity:.7 }
.ds-time-title{ margin:.1em 0 .3em }
.ds-time-text{ margin:0; opacity:.85 }

/* Alternating: the rule runs up the middle and the entries take turns.
   One column on a narrow screen, because two columns of half a sentence
   each is not a timeline. */
@media (min-width:721px){
  .ds-time-alt .ds-time-list{ display:grid; grid-template-columns:1fr 1fr; column-gap:0 }

  /* Each entry on a line of its own, numbered in the markup.
     Left to place itself a grid fills the line it is on before starting
     another, so the second entry sat beside the first instead of below it
     and the two sides read as pairs rather than as one run of dates. */
  .ds-time-alt .ds-time-row{
    grid-row:var(--ds-time-n, auto);
    grid-column:1;
    padding:0 34px clamp(20px,3vw,34px) 0;
    text-align:right;
  }
  .ds-time-alt .ds-time-row:nth-child(even){
    grid-column:2;
    padding:0 0 clamp(20px,3vw,34px) 34px;
    text-align:left;
  }

  /* The segments meet on the line down the middle. Both sides resolve to the
     same place because that is where the two columns meet. */
  .ds-time-alt .ds-time-row::before{ left:auto; right:-1px }
  .ds-time-alt .ds-time-row:nth-child(even)::before{ right:auto; left:-1px }
  .ds-time-alt .ds-time-row:last-child{ padding-bottom:0 }

  /* Both sides meet on that line. The offset is the row's indent plus half
     the marker, so it straddles the rule from either side. */
  .ds-time-alt .ds-time-dot{ left:auto; right:-40px }
  .ds-time-alt .ds-time-row:nth-child(even) .ds-time-dot{ right:auto; left:-40px }
}

/* Armed for the reveal. Readable with no script, as everything else is. */
.ds-fx-timeline[data-anim="yes"].is-armed .ds-time-row{ opacity:0 }

@media (prefers-reduced-motion:reduce){
  .ds-fx-timeline[data-anim="yes"].is-armed .ds-time-row{ opacity:1 }
}
