/* ==========================================================================
   Rotating drum 1.0 — a services grid built around a scroll-driven 3D drum
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   Eight pill-shaped blades hang from one shared horizontal hinge, 45 degrees
   apart. Scrolling the page past the stage turns the whole set around that
   hinge, so blades rise into view, face the reader, and tip away again.

   The drum is decorative motion, never a control: the eight words are plain
   text in the document, so they are read in order whether or not the script
   runs, whether or not the reader can see the rotation at all. Without
   JavaScript the drum simply holds its resting angle, which is chosen so one
   blade faces the reader square on.

   PROPORTIONS
   Every metric derives from --sd-font-size, the size of the body copy inside
   a card. The ratios were measured on the reference implementation at 1440px
   (see README for the full table); they are what makes the component look
   like itself at any scale.

     body copy         1                 (the reference)
     column           27 em
     grid gutter       2 em
     card width       25 em
     card gutter       1.5 em
     card heading      3 em
     icon              2.5 em
     stage            27 em wide, 50 em tall
     stage radius      2.5 em
     perspective      62.5 em
     blade            15 em wide, 20 em tall
     blade drop        1 em              (below the hinge)
     blade word        2 em
     notch             8 em by 6 em, 3 em radius

   So setting --sd-font-size rescales the whole component — the drum and its
   perspective included, not just the text.

   ANGLES
     --sd-blade-step   45deg   360 / 8, the angle between two blades
     --sd-sweep       102deg   turned over one full crossing of the viewport
     --sd-phase        39deg   angle at the moment the stage enters from below

   Phase and sweep are chosen together so that at mid-crossing the drum sits
   at 39 + 51 = 90deg, an exact multiple of the blade step: a blade faces the
   reader square on precisely when the stage is centred.

   Order of this file:
     1. Design tokens
     2. Section shell and heading
     3. Grid
     4. Cards
     5. Stage, perspective and drum
     6. Blades
     7. Notch and its link
     8. Responsive: one column, then compact
     9. Fallbacks: reduced motion, forced colours, no JavaScript

   Naming follows BEM: .rotating-drum, .rotating-drum__part, .rotating-drum--variant.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

.rotating-drum {
  /* Scale. Everything below is calc()ed against this, in absolute units, so
     that a custom property is never resolved against the font size of
     whichever element happens to consume it. */
  --sd-font-size: 1rem;

  /* Ink and ground */
  --sd-bg: #f2f2f2;
  --sd-ink: #000;
  --sd-ink-soft: #6e6e6e;
  --sd-rule: #e6e6e6;
  --sd-stage-bg: #000;
  --sd-blade-from: #000;
  /* Deep ember, the house accent taken down to where white text still holds:
     9.85:1 against the label. The blade is a gradient from black, so the word
     sits on the darkest part of it — measured, not guessed. */
  --sd-blade-to: #7d2410;
  --sd-blade-ink: #fff;
  --sd-focus: #7d2410;

  /* Type. The display face is condensed and very heavy; the body face is the
     same family at a normal width. Both are declared in the demo, not here,
     so the component inherits whatever the host page provides if it prefers.

     Display text is set at weight 900, not 800. The reference asks for 800,
     but its own @font-face table binds weight 700 to the Bold Narrow file,
     so nothing answers 800 and the engine resolves upwards to the Black
     Narrow file. Measured on the one blade that faces the reader square on —
     the only one whose box is not foreshortened — 900 reproduces the word to
     the pixel (103px wide) where 800 falls 4px short. */
  --sd-font-display: "Rotating drum Narrow", "Arial Narrow", Impact, sans-serif;
  --sd-font-body: "Rotating drum", system-ui, -apple-system, "Segoe UI", sans-serif;
  --sd-font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;

  /* Geometry */
  --sd-column: calc(27 * var(--sd-font-size));
  --sd-gutter: calc(2 * var(--sd-font-size));
  --sd-card-width: calc(25 * var(--sd-font-size));
  --sd-card-gutter: calc(1.5 * var(--sd-font-size));
  --sd-stage-height: calc(50 * var(--sd-font-size));
  --sd-stage-radius: calc(2.5 * var(--sd-font-size));
  --sd-perspective: calc(62.5 * var(--sd-font-size));
  --sd-blade-width: calc(15 * var(--sd-font-size));
  --sd-blade-height: calc(20 * var(--sd-font-size));
  --sd-blade-drop: calc(1 * var(--sd-font-size));
  --sd-notch-width: calc(8 * var(--sd-font-size));
  --sd-notch-height: calc(6 * var(--sd-font-size));
  --sd-notch-radius: calc(3 * var(--sd-font-size));
  --sd-pad: calc(2.5 * var(--sd-font-size));

  /* Rotation. --sd-angle is the one value the script writes; everything else
     is fixed. The default is mid-crossing, so a blade faces the reader even
     when nothing ever moves it. */
  --sd-blade-step: 45deg;
  --sd-sweep: 102deg;
  --sd-phase: 39deg;
  --sd-angle: 90deg;

  /* Motion. The inertia of the drum is a bare number, not a measure, so it
     lives in data-ease on the block rather than here. */
  --sd-link-duration: 0.35s;

  box-sizing: border-box;
  display: block;
  width: 100%;
  padding: calc(5 * var(--sd-font-size)) var(--sd-pad);
  background: var(--sd-bg);
  color: var(--sd-ink);
  font-family: var(--sd-font-body);
  font-size: var(--sd-font-size);
  line-height: 1.5;
}

/* The component declares its own box model rather than assuming a reset. */
.rotating-drum *,
.rotating-drum *::before,
.rotating-drum *::after {
  box-sizing: inherit;
}

/* ==========================================================================
   2. Section shell and heading
   ========================================================================== */

.rotating-drum__inner {
  width: 100%;
  max-width: calc(100 * var(--sd-font-size));
  margin-inline: auto;
}

.rotating-drum__eyebrow {
  /* No reset is assumed: a <p> keeps its default bottom margin otherwise,
     and the heading below lands 16px too low. */
  margin: 0 0 var(--sd-gutter);
  font-family: var(--sd-font-mono);
  font-size: calc(0.875 * var(--sd-font-size));
  letter-spacing: 0.04em;
  text-align: center;
  text-transform: uppercase;
}

.rotating-drum__asterisk {
  font-size: 1.25em;
  line-height: 0;
  vertical-align: -0.1em;
}

.rotating-drum__heading {
  margin: 0;
  font-family: var(--sd-font-display);
  font-size: calc(10 * var(--sd-font-size));
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.0334em;
  text-align: center;
  text-transform: uppercase;
  text-wrap: balance;
}

.rotating-drum__heading b {
  font-weight: inherit;
  color: var(--sd-ink-soft);
}

/* ==========================================================================
   3. Grid
   --------------------------------------------------------------------------
   Three columns, two rows. The stage takes the middle column across both
   rows; the four cards fill the outer columns by auto-placement, which walks
   around the explicitly placed stage on its own.
   ========================================================================== */

.rotating-drum__grid {
  display: grid;
  /* minmax(0, 1fr) rather than 1fr: a bare 1fr floors at min-content and a
     long unbreakable word would push the column wider than its share. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sd-gutter);
  align-items: center;
  margin-top: calc(4 * var(--sd-font-size));
}

/* ==========================================================================
   4. Cards
   ========================================================================== */

.rotating-drum__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sd-card-gutter);
  width: 100%;
  max-width: var(--sd-card-width);
  margin-inline: auto;
  text-align: center;
}

.rotating-drum__icon {
  display: block;
  width: calc(2.5 * var(--sd-font-size));
  height: calc(2.5 * var(--sd-font-size));
  flex: none;
  color: var(--sd-ink);
}

.rotating-drum__card-title {
  margin: 0;
  font-family: var(--sd-font-display);
  font-size: calc(3 * var(--sd-font-size));
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.0334em;
  text-transform: uppercase;
  text-wrap: balance;
}

.rotating-drum__rule {
  width: 100%;
  height: 1px;
  margin: 0;
  border: 0;
  background: var(--sd-rule);
}

.rotating-drum__card-text {
  margin: 0;
  font-size: calc(1 * var(--sd-font-size));
  line-height: 1.5;
}

/* ==========================================================================
   5. Stage, perspective and drum
   ========================================================================== */

.rotating-drum__stage {
  /* Explicit placement: middle column, both rows. Everything else falls into
     place around it without a single nth-child rule. */
  grid-area: 1 / 2 / 3 / 3;
  position: relative;
  width: 100%;
  max-width: var(--sd-column);
  height: var(--sd-stage-height);
  margin-inline: auto;
  border-radius: var(--sd-stage-radius);
  background: var(--sd-stage-bg);
  /* No clipping here, on purpose. The notch below fills the bottom-right
     corner and its own corner is square: it has to overflow the stage's
     rounding, exactly as in the reference. Clip the stage and that corner
     gets rounded off instead — the boxes still measure flush to the pixel,
     only the shape changes, which is why a box comparison never catches it.
     The blades are clipped one level down, by the viewport. */
}

.rotating-drum__viewport {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  perspective: var(--sd-perspective);
  /* This is where the rounding bites: the blades stay inside the stage's
     rounded shape, while the notch, which is not in here, keeps its square
     corner. clip rather than hidden — nothing here should ever scroll. */
  border-radius: inherit;
  overflow: clip;
}

.rotating-drum__drum {
  /* The hinge. Zero height on purpose: the blades are absolutely positioned
     from its top edge, so this element is nothing but an axis and an origin.
     A <ul> keeps the eight words a list for anything that reads the page. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  transform-style: preserve-3d;
  transform-origin: 50% 0;
  transform: rotateX(var(--sd-angle));
  /* Layout containment, and it earns its place: a blade turned near the
     quarter projects some 360px wide through the perspective, which the
     engine counts as layout overflow on every ancestor. overflow:clip hides
     that without removing it — scrollWidth still reports ~109px too wide on
     the stage. Containing the layout here stops it at the source, and does
     not flatten preserve-3d (the bench checks both). */
  contain: layout;
}

/* ==========================================================================
   6. Blades
   --------------------------------------------------------------------------
   Each blade is hinged at its own top edge, on the shared axis, and turned by
   its rank times the step. The pill inside hangs one em below that hinge,
   which is the gap you see between two neighbouring blades.
   ========================================================================== */

.rotating-drum__blade {
  position: absolute;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sd-blade-width);
  height: var(--sd-blade-height);
  transform-style: preserve-3d;
  transform-origin: 50% 0;
  transform: rotateX(calc(var(--sd-rank) * var(--sd-blade-step) * -1));
}

.rotating-drum__blade:nth-child(1) { --sd-rank: 0; }
.rotating-drum__blade:nth-child(2) { --sd-rank: 1; }
.rotating-drum__blade:nth-child(3) { --sd-rank: 2; }
.rotating-drum__blade:nth-child(4) { --sd-rank: 3; }
.rotating-drum__blade:nth-child(5) { --sd-rank: 4; }
.rotating-drum__blade:nth-child(6) { --sd-rank: 5; }
.rotating-drum__blade:nth-child(7) { --sd-rank: 6; }
.rotating-drum__blade:nth-child(8) { --sd-rank: 7; }

.rotating-drum__pill {
  position: relative;
  top: var(--sd-blade-drop);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: calc(62.5 * var(--sd-font-size));
  background-image: linear-gradient(var(--sd-blade-from), var(--sd-blade-to));
}

.rotating-drum__word {
  font-family: var(--sd-font-display);
  font-size: calc(2 * var(--sd-font-size));
  font-weight: 900;
  line-height: 1;
  color: var(--sd-blade-ink);
  text-transform: uppercase;
}

/* Blades turned past the quarter show their back, and their word reads
   mirrored — that is simply what a rotating solid does, and it is what the
   reference implementation shows. Set data-backface="hidden" on the block to
   drop the far side instead. */
.rotating-drum[data-backface="hidden"] .rotating-drum__pill {
  backface-visibility: hidden;
}

/* ==========================================================================
   7. Notch and its link
   --------------------------------------------------------------------------
   A rectangle of page colour bitten out of the stage's bottom-right corner,
   with two small fillets that turn the two inner right angles into curves
   flowing the other way. The link sits inside it at 90%.
   ========================================================================== */

.rotating-drum__notch {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
  width: var(--sd-notch-width);
  height: var(--sd-notch-height);
  border-top-left-radius: var(--sd-notch-radius);
  background: var(--sd-bg);
}

/* The two fillets hollow out the corner where the button sits: each paints the
   page colour over the stage, leaving a concave quarter behind.

   They are a pixel larger than the radius they fill, and pushed a pixel past
   the stage. Cut to the exact size, the filled part of the path ends precisely
   on the stage's edge, and the antialiasing of that edge blends the two: a grey
   seam one physical pixel wide, measured at #898989 over a #f2f2f2 ground —
   a black hairline drawn around the rounded corner. The overflow lands on bare
   page on both sides, so nothing of it shows.

   The path fills its box on the left and bottom edges, which the -90° turn puts
   at the bottom and the right; those are the two sides that grow. */
.rotating-drum__fillet {
  position: absolute;
  width: calc(var(--sd-stage-radius) + 1px);
  height: calc(var(--sd-stage-radius) + 1px);
  color: var(--sd-bg);
}

/* Above the notch, tucked against its right edge. */
.rotating-drum__fillet--top {
  top: calc(var(--sd-stage-radius) * -1);
  right: -1px;
  transform: rotate(-90deg);
}

/* Left of the notch, sitting on the stage's bottom edge. */
.rotating-drum__fillet--left {
  bottom: -1px;
  left: calc(var(--sd-stage-radius) * -1);
  transform: rotate(-90deg);
}

.rotating-drum__link {
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(0.75 * var(--sd-font-size));
  width: 90%;
  height: 90%;
  border-radius: var(--sd-notch-radius);
  background: var(--sd-stage-bg);
  color: var(--sd-blade-ink);
  font-size: calc(0.875 * var(--sd-font-size));
  line-height: 1.2;
  text-decoration: none;
  transition: background-color var(--sd-link-duration);
}

.rotating-drum__link:hover {
  background: var(--sd-blade-to);
}

.rotating-drum__link:focus-visible {
  outline: calc(0.1875 * var(--sd-font-size)) solid var(--sd-blade-ink);
  outline-offset: calc(-0.375 * var(--sd-font-size));
}

.rotating-drum__arrow {
  width: calc(1.125 * var(--sd-font-size));
  height: calc(1.125 * var(--sd-font-size));
  flex: none;
  transition: transform var(--sd-link-duration);
}

.rotating-drum__link:hover .rotating-drum__arrow {
  transform: translateX(calc(0.25 * var(--sd-font-size)));
}

/* ==========================================================================
   8. Responsive
   --------------------------------------------------------------------------
   Below the three-column breakpoint everything stacks, and the stage takes
   its turn in the flow between the second and third card. Type shrinks with
   --sd-font-size alone, which is what carries the drum down with it.
   ========================================================================== */

@media (max-width: 62rem) {
  .rotating-drum {
    --sd-font-size: 0.9375rem;
    --sd-pad: calc(1.5 * var(--sd-font-size));
  }

  .rotating-drum__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .rotating-drum__heading {
    font-size: calc(6 * var(--sd-font-size));
  }

  .rotating-drum__card-title {
    font-size: calc(2.5 * var(--sd-font-size));
  }

  /* Back into the flow, third of five rows. */
  .rotating-drum__stage {
    grid-area: auto;
    order: 0;
    max-width: calc(30 * var(--sd-font-size));
  }

  .rotating-drum__card:nth-of-type(1) { order: -2; }
  .rotating-drum__card:nth-of-type(2) { order: -1; }
  .rotating-drum__card:nth-of-type(3) { order: 1; }
  .rotating-drum__card:nth-of-type(4) { order: 2; }
}

@media (max-width: 30rem) {
  .rotating-drum {
    --sd-font-size: 0.8125rem;
    --sd-stage-height: calc(40 * var(--sd-font-size));
  }

  .rotating-drum__heading {
    font-size: calc(4.5 * var(--sd-font-size));
  }

  .rotating-drum__card-title {
    font-size: calc(2.25 * var(--sd-font-size));
  }
}

/* ==========================================================================
   9. Fallbacks
   ========================================================================== */

/* Reduced motion: the drum stops following the scroll and rests where a blade
   faces the reader. The gradient and the shape stay — they are a picture, not
   a movement. Durations and delays both go to zero: a delay is movement
   spread over time. */
@media (prefers-reduced-motion: reduce) {
  .rotating-drum {
    --sd-angle: 90deg;
    --sd-link-duration: 0.01ms;
  }

  .rotating-drum__arrow,
  .rotating-drum__link {
    transition-delay: 0s;
  }
}

/* Forced colours: the gradient is dropped by the platform, so the blades need
   a border to stay separate shapes, and the words need a system ink. */
@media (forced-colors: active) {
  .rotating-drum__pill {
    border: 1px solid CanvasText;
    background: Canvas;
  }

  .rotating-drum__word {
    color: CanvasText;
  }

  .rotating-drum__link {
    border: 1px solid LinkText;
    color: LinkText;
  }

  .rotating-drum__rule {
    background: CanvasText;
  }
}
