/* ---------- Design-system unification (2026-09-03) — this file is now
   loaded AFTER css/styles.css on the pages that use it (the homepage and
   about.html), not standalone. It no longer redeclares anything
   styles.css already provides at the same values (base reset, --c-*
   colors, --paper, type scale) — only overrides what should genuinely
   differ (Redaction for headings, the uncapped .wrap), plus the
   components styles.css has no equivalent for (nav chrome for these two
   pages, gooey buttons, and every section-specific class). See
   Docs/2026-09-03-site-restructure-plan.md, Part 1, for the reasoning.
   styles.css itself is untouched, so every legacy page is unaffected. ---------- */

@font-face {
  font-family: 'Redaction';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/Redaction-Regular.woff2') format('woff2');
}

:root {
  /* headings only — body text stays on styles.css's --font-body */
  --font-display: 'Redaction', ui-serif, Georgia, serif;
  --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  /* v7's navy ink, kept instead of styles.css's warm-black default */
  --ink: var(--c-navy);
  --ink-soft: var(--c-navy);
}

/* styles.css's own html/body rules already cover the reset (margin,
   --paper background, --ink color, --font-body, antialiasing) — the one
   thing they don't cover is this, needed because a couple of this file's
   sections (the mask-reveal transforms, the polaroid gallery's rotated
   cards) can transiently render wider than the viewport mid-interaction. */
html,
body {
  overflow-x: hidden;
}

.wrap {
  max-width: 88vw;
  margin: 0 auto;
}

/* about.html Milestone timeline — styles.css's .timeline-item has no
   image slot (the legacy about.html timeline was text-only); this adds
   one, styled as an explicit placeholder until real photos exist. */
.timeline-photo {
  width: 100%;
  aspect-ratio: 4 / 3;
  margin-bottom: 12px;
  background: var(--line-soft);
  border: 1px dashed var(--ink-faint);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 8px;
  font-size: 12px;
  color: var(--ink-faint);
}

/* about.html Tentang — force-directed node graph sits beside the
   description, ported from a CodePen (d3.js force-directed graph with
   randomly-colored circles as nodes). Recolored to the site palette,
   text labels removed — see Docs/2026-09-03-site-restructure-plan.md. */
.about-content-row {
  display: flex;
  align-items: flex-start;
  gap: 40px;
}

.about-desc {
  flex: 1 1 560px;
  min-width: 0;
}

.about-graph {
  flex: 1 1 380px;
  position: relative;
  width: 100%;
  aspect-ratio: 5 / 4;
  min-height: 320px;
}

.about-graph svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

@media (max-width: 860px) {
  .about-content-row {
    flex-direction: column;
  }

  .about-graph {
    width: 100%;
  }
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 20px;
  padding: 16px 30px;
  border-radius: 999px;
  text-decoration: none;
  cursor: pointer;
  border: 1.5px solid currentColor;
  background: transparent;
  color: inherit;
  transition: transform .2s ease, background .2s ease, color .2s ease;
}

.btn:hover {
  transform: translateY(-2px);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.btn-solid {
  background: var(--ink);
  color: var(--paper-text);
  border-color: var(--ink);
}

.btn-solid:hover {
  background: var(--c-terracotta);
  border-color: var(--c-terracotta);
  color: var(--paper-text);
}

/* section 1 is now a dark (navy) section like 2/4/5 — its two buttons get
   the same light-on-dark treatment as those sections' CTAs. Primary
   ("Baca konteksnya") inverts .btn-solid's default navy fill to a paper
   fill so it doesn't blend into the section background; secondary
   ("Tentang Riset") is a plain outline that already inherits .s1's paper
   text/border color, it just needs a visible (paper-fill) hover to match. */
.s1 .btn-solid {
  background: var(--paper-text);
  color: var(--c-navy);
  border-color: var(--paper-text);
}

.s1 .btn-solid:hover {
  background: var(--c-terracotta);
  border-color: var(--c-terracotta);
  color: var(--paper-text);
}

.s1 .btn:not(.btn-solid):hover {
  background: var(--paper-text);
  color: var(--c-navy);
  border-color: var(--paper-text);
}

/* SVG holding the shared #goo filter def — zero-size already keeps it out
   of layout, this just also keeps it out of the paint/hit-test tree. */
.visually-hidden-defs {
  visibility: hidden;
  position: absolute;
}

/* ---------- gooey CTA buttons — ported from "Gooey button hover effect
   with SVG filters & CSS" (https://css-tricks.com/gooey-effect/): two
   blob pseudo-elements scale in from 0 on hover, and the shared SVG
   filter (blur + contrast the alpha channel back up) melts their edges
   into the button's own shape instead of overlapping it as flat circles.
   Colored per button to match whatever its own hover-fill color already
   is, so the blobs read as the button growing new lobes, not a
   mismatched color arriving separately. ---------- */
/* filter only while hovered/focused: an outline (unfilled) button run
   through the goo filter at rest is invisible — its thin 1.5px border
   doesn't have enough opaque coverage to survive the filter's blur +
   alpha-boost math, only the solid blobs do. Solid buttons don't have
   this problem, but scoping to :hover keeps the effect exactly what it
   says on the tin — a hover effect — for either kind. */
.gooey-wrap {
  display: inline-block;
}

.gooey-wrap:hover,
.gooey-wrap:focus-within {
  filter: url('#goo');
}

.gooey-wrap .btn {
  position: relative;
  z-index: 1;
}

.gooey-wrap .btn::before,
.gooey-wrap .btn::after {
  content: '';
  position: absolute;
  display: block;
  border-radius: 50%;
  width: 2.5em;
  height: 1.9em;
  background: var(--gooey-color, currentColor);
  transform: scale(0);
  transition: transform 0.85s ease;
  z-index: -1;
}

.gooey-wrap .btn::before {
  top: -40%;
  left: 8%;
}

.gooey-wrap .btn::after {
  bottom: -40%;
  right: 8%;
}

.gooey-wrap:hover .btn::before,
.gooey-wrap:hover .btn::after {
  transform: scale(1);
}

.gooey-terracotta {
  --gooey-color: var(--c-terracotta);
}

.gooey-navy {
  --gooey-color: var(--c-navy);
}

.gooey-paper {
  --gooey-color: var(--paper-text);
}

/* Arsip (article.html) filter tabs — restyled to match the subnav pill
   buttons on Tentang (about.html, .subnav .btn.btn-secondary): same
   pill shape, border, hover-lift, and active-state invert. Kept as
   .tab/.tab.active rather than switching to .btn, since the tab-filter
   script already targets those classes directly. */
.tab {
  border-radius: 999px;
  padding: 13px 22px;
  border: 1.5px solid var(--ink);
  color: var(--ink);
  font-weight: 700;
  transition: transform .2s ease, background .2s ease, color .2s ease;
}

.tab:hover {
  transform: translateY(-2px);
}

.tab.active {
  background: var(--ink);
  color: var(--paper-text);
  border-color: var(--ink);
}

/* ---------- nav ---------- */
/* Transparent floating header, absolutely positioned so it sits over the
   hero's particle background with no box behind it — only makes sense
   with a hero underneath, so it's scoped to the homepage. Every other
   page keeps styles.css's default sticky, in-flow, solid-background
   header (about.html included) so content isn't pushed under it.
   background/border-bottom explicitly cleared too: styles.css's .sitehead
   sets both, which blended invisibly back when the hero was the same
   cream as the header — now that the hero is navy, that opaque cream
   band would show as a visible strip above it otherwise. */
body.page-home .sitehead {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  background: transparent;
  border-bottom: none;
}

/* section 1 (the hero underneath) is now navy, so the floating header's
   default navy text/icons would disappear into it — flip to light just
   on the homepage. Every other page keeps the default dark-on-light
   header untouched. */
body.page-home .sitehead .wrap,
body.page-home .navtoggle {
  color: var(--paper-text);
}

body.page-home .navtoggle {
  border-color: rgba(237, 227, 200, 0.4);
}

body.page-home .wordmark-badge {
  color: var(--paper-text);
  border-color: rgba(237, 227, 200, 0.4);
}

.sitehead .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 84px;
  gap: 16px;
  color: var(--ink);
}

.wordmark {
  font-family: var(--font-display);
  font-size: 1.25rem;
  text-decoration: none;
  color: inherit;
}

/* desktop-only — this used to apply at every width (including mobile,
   since it had no media guard), fighting styles.css's own working
   full-screen colour-band mobile menu for the same properties (wrong
   link colour/opacity/size, and a differently-positioned hamburger
   icon that never got the body.nav-open morph-to-X rules). Scoping it
   above styles.css's own 860px breakpoint leaves mobile entirely to
   styles.css's version everywhere, desktop unchanged on these pages. */
@media (min-width: 861px) {
  .navlinks {
    display: flex;
    gap: 26px;
    font-size: 14px;
  }

  .navlinks a {
    text-decoration: none;
    opacity: 0.8;
    color: inherit;
  }

  .navlinks a:hover {
    opacity: 1;
  }
}

/* ---------- section 1 — hero, tsParticles on a navy ground (matches
   section 2's dark treatment) ---------- */
.s1 {
  position: relative;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  background: var(--c-navy);
  color: var(--paper-text);
  overflow: hidden;
}

#s1-particles {
  position: absolute;
  inset: 0;
  z-index: 0;
}

/* below 640px the headline can grow tall enough that flex-centering
   (.s1's align-items:center) overflows above the viewport, running
   under the floating transparent header. Anchor from the top instead,
   with enough clearance for the header's own height. */
@media (max-width: 640px) {
  .s1 {
    align-items: flex-start;
  }
}

/* dotted 3D globe, ported from a CodePen (3D Globe Three.js with
   location pointer) — lives in section 3 now (used to be a full-bleed
   hero backdrop, pinned to the bottom edge and cropped to a horizon
   sliver; moved here per request and shown whole instead, since it's a
   card in a two-column row now, not an atmospheric background). A plain
   square box is enough to show the whole sphere — the "cropped to a
   sliver" look was purely the old absolute-position + overflow:hidden
   trick, not a camera limitation, so removing that trick is all that's
   needed for the full sphere to render here. */
/* wrap wears the flex sizing — flex-basis:0 with the same grow/shrink
   factor as .s3-left (see its own comment) splits the row 50/50 between
   text and globe, per request, rather than sizing off either side's
   content — and is the positioning context for the decorative floats
   around it; .s3-globe itself just fills the wrap at a fixed 1:1
   aspect-ratio so the canvas/overlay math (which reads .s3-globe's own
   box) is unaffected by the floats sharing its parent. */
.s3-globe-wrap {
  position: relative;
  flex: 1 1 0;
  max-width: 1100px;
  margin: 0 auto;
}

.s3-globe {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  cursor: grab;
}

.s3-globe:active {
  cursor: grabbing;
}

.s3-globe canvas {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* same flex-basis-resolves-against-the-wrong-axis fix .ig-post needed
   (see its own comment below) — .s3-layout goes column below 900px too.
   width:100% is also needed explicitly here (not just max-width) — as a
   stretched flex item with no width of its own, .s3-globe-wrap's
   resolved width was landing on 0 in testing, which then made .s3-globe
   (width:100% of it) 0 too, and the globe silently failed to render on
   mobile even though the canvas/JS were working fine. An explicit width
   breaks that circular 0-width resolution. */
@media (max-width: 900px) {
  .s3-globe-wrap {
    flex: none;
    width: 100%;
    max-width: 100%;
  }
}

/* decorative floats around the globe — same .s1-float engine as the hero
   (bob animation + drag-anywhere-within-parent), reused here since
   .s3-globe-wrap is its own position:relative context; only placement/
   size differ per shape. Sit outside .s3-globe itself (which clips to a
   circle-safe square via overflow:hidden) so they're never cropped. */
.s3-float-a {
  top: -7%;
  left: 2%;
  width: clamp(46px, 5vw, 82px);
  animation-delay: -2s;
}

.s3-float-b {
  bottom: -5%;
  right: 0%;
  width: clamp(42px, 4.4vw, 74px);
  animation-delay: -5s;
}

.s3-float-c {
  top: 14%;
  right: -7%;
  width: clamp(50px, 5.4vw, 88px);
  animation-delay: -1s;
}

@media (max-width: 900px) {
  .s3-float-a,
  .s3-float-b,
  .s3-float-c {
    display: none;
  }
}

/* click-to-drop-a-pointer overlay, ported from the source — this SVG
   draws the connector line (an <svg><path> now, not a second canvas —
   see the markup comment), the popup shows the clicked point's lat/
   long. Both sit above the 3D canvas but never intercept input
   themselves, so dragging still reaches the globe. */
#s1-globe-2d-overlay,
#s1-globe-popup-overlay {
  pointer-events: none;
}

#s1-globe-2d-overlay {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

#s1-globe-popup-overlay {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.s1-globe-popup {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  background-color: var(--paper);
  opacity: 0;
  color: var(--c-navy);
  font-family: var(--font-body);
  padding: 6px 10px;
  font-size: 13px;
  border-radius: 3px;
  filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, .4));
  white-space: normal;
  max-width: 190px;
  line-height: 1.4;
}

/* decorative floating collage shapes in the hero — small, gently
   bobbing, and draggable (drag handler is the "section 1: floating
   collage shapes" IIFE in index.html's own script block, right after
   the hero globe). Positioned with top/left/right/bottom so the JS only
   has to switch to pixel-based left/top once a drag actually starts
   (see toPixelPosition there). */
.s1-float {
  position: absolute;
  z-index: 3;
  width: clamp(90px, 9vw, 160px);
  height: auto;
  cursor: grab;
  touch-action: none;
  filter: drop-shadow(0 8px 20px rgba(0, 0, 0, .35));
  animation: s1FloatBob 6s ease-in-out infinite;
  user-select: none;
}

.s1-float.is-dragging {
  cursor: grabbing;
  animation-play-state: paused;
}

.s1-float-a {
  top: 20%;
  left: 6%;
  animation-delay: 0s;
}

.s1-float-b {
  bottom: 12%;
  right: 6%;
  width: clamp(80px, 8vw, 140px);
  animation-delay: -3s;
}

/* the 5 smaller collage pieces (star, sparkle, scallop, and two loose
   ribbon squiggles) — same family as a/b (box/pyramid) but sized down
   since 7 pieces at full size would crowd the headline. */
.s1-float-c {
  top: 9%;
  right: 11%;
  width: clamp(48px, 5vw, 80px);
  animation-delay: -1s;
}

.s1-float-d {
  bottom: 30%;
  left: 3%;
  width: clamp(44px, 4.5vw, 70px);
  animation-delay: -4s;
}

/* e1/e2 are both 5.png, stacked by default a little lower than a and b
   respectively (same left/right, offset down so they peek out under the
   box/pyramid instead of centering on it), higher z-index so they paint
   over the box/pyramid — still their own independently draggable
   .s1-float. */
.s1-float-e1 {
  top: 28%;
  left: 6%;
  z-index: 4;
  width: clamp(70px, 7vw, 125px);
  animation-delay: -2s;
}

.s1-float-e2 {
  bottom: 9%;
  right: 6%;
  z-index: 4;
  width: clamp(60px, 6vw, 105px);
  animation-delay: -3.5s;
}

.s1-float-f {
  top: 10%;
  left: 36%;
  width: clamp(80px, 9vw, 150px);
  animation-delay: -5s;
}

.s1-float-g {
  bottom: 7%;
  right: 32%;
  width: clamp(90px, 10vw, 160px);
  animation-delay: -1.5s;
}

@keyframes s1FloatBob {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50% { transform: translateY(-14px) rotate(3deg); }
}

/* below 640px the headline/lede stack tall enough (see .s1 .wrap's
   padding-top above) that a top-anchored float-a collides with the
   text. Both shapes move to the bottom corners instead, the same
   proven-clear zone float-b already uses above this breakpoint. */
@media (max-width: 640px) {
  .s1-float {
    width: clamp(64px, 22vw, 110px);
  }

  .s1-float-a {
    top: auto;
    left: 4%;
    bottom: 4%;
  }

  .s1-float-b {
    width: clamp(56px, 20vw, 96px);
  }

  /* e1/e2 track a/b's mobile position, offset a little lower (same idea
     as the desktop rule above), and sized down further since they're
     the topping accent, not the base shape. */
  .s1-float-e1 {
    top: auto;
    left: 4%;
    bottom: 1%;
    width: clamp(50px, 18vw, 88px);
  }

  .s1-float-e2 {
    width: clamp(44px, 16vw, 78px);
  }

  /* the rest stay desktop-only — on a phone-height hero there isn't a
     text-free spot left for all 9, and a/b (+ e1/e2 stacked on them)
     already carry the collage feel at this size. */
  .s1-float-c,
  .s1-float-d,
  .s1-float-f,
  .s1-float-g {
    display: none;
  }
}

.s1 .wrap {
  position: relative;
  z-index: 1;
  padding: 20px 0 60px;
  text-align: center;
  /* the headline/lede column wraps to enough lines on narrow screens
     that its box covers almost the whole hero, including most of the
     globe's visible arc underneath — pointer-events:none here lets
     drags pass through all that empty space to the globe, with the
     buttons opting back in below so they stay clickable. */
  pointer-events: none;
}

/* below 640px .s1 switches to align-items:flex-start (see the media
   query above) so this needs its own top clearance for the floating
   header instead of relying on centering to push it down. */
@media (max-width: 640px) {
  .s1 .wrap {
    padding-top: 110px;
  }
}

.s1 .ctas,
.s1 .ctas a {
  pointer-events: auto;
}

.s1 h1 {
  font-size: clamp(2.6rem, 6.4vw, 7.2rem);
  line-height: 1.05;
  max-width: 20ch;
  margin: 0 auto 22px;
}

.s1 p.lede {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: var(--paper-text);
  margin: 60px auto 70px;
}

.s1 .ctas {
  display: flex;
  gap: 14px;
  justify-content: center;
  flex-wrap: wrap;
}

.s1 .scrolldown {
  position: absolute;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(237, 227, 200, 0.6);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.s1 .scrolldown .line {
  width: 1px;
  height: 26px;
  background: currentColor;
  animation: scrolldown-pulse 1.8s ease-in-out infinite;
}

@keyframes scrolldown-pulse {

  0%,
  100% {
    opacity: 0.25;
  }

  50% {
    opacity: 0.9;
  }
}

@media (prefers-reduced-motion: reduce) {
  .s1 .scrolldown .line {
    animation: none;
  }
}

/* ---------- section 2 — GSAP mask-mouse-effect, ported straight from
   the original CodePen structure (content / hidden-content, two text
   rows, hover-to-enlarge on the paragraphs, hover-to-shrink-and-snap on
   the link) instead of the single-headline rebuild from before — only
   the colors are swapped for the palette, and the mask math is section-
   relative (not page-relative like the original single-page demo,
   since this is one section inside a much longer page) ---------- */
.s2 {
  position: relative;
  background: var(--c-navy);
  color: var(--paper-text);
  overflow: hidden;
  /* hairline seam between sections 1 and 2 — both are navy now, so
     there's no color break marking where one ends and the other starts */
  border-top: 1px solid rgba(237, 227, 200, 0.15);
}

.s2-content,
.s2-hidden,
.s2b-content {
  padding: 80px 6vw;
}

.s2-row {
  min-height: 30vh;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0;
}

.s2-line {
  font-family: var(--font-display);
  font-weight: 500;
  text-align: left;
  font-size: clamp(1.7rem, 4vw, 3.1rem);
  line-height: 1.35;
  max-width: 80%;
  margin: 0;
}

.s2-content .s2-line,
.s2b-content .s2-line {
  color: var(--paper-text);
}

.s2-headline {
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  max-width: 30ch;
}

.s2-link-row {
  text-align: left;
  padding: 8px 0 42px;
}

.s2-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: inherit;
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* vertical-only — a horizontal 10px here pushed the text out of line
     with the paragraph directly above it, which has no left padding. */
  padding: 10px 0;
  cursor: pointer;
}

.s2-link-icon {
  font-size: 20px;
  line-height: 1;
}

.s2-hidden {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  visibility: hidden;
  --x: 0px;
  --y: 0px;
  --size: 5px;
  background: linear-gradient(180deg, var(--c-mustard) 0%, var(--c-terracotta) 45%, var(--c-plum) 100%);
  color: var(--c-navy);
  -webkit-mask-image: radial-gradient(circle at var(--x) var(--y), black var(--size), transparent calc(var(--size) + 1px));
  mask-image: radial-gradient(circle at var(--x) var(--y), black var(--size), transparent calc(var(--size) + 1px));
}

.s2-hidden .s2-line {
  color: var(--c-navy);
}

.s2-hidden .s2-link {
  color: var(--c-navy);
}

/* ---------- section 2b — the 5 pillars that built the current legitimacy
   system. Intentionally NOT the S3 sticky-note style (that's reserved for
   grassroots practices) — numbered, bordered cards read as structural/
   institutional instead of informal. Each pillar borrows one accent color
   from S3's practice cards (same order) so the two card grids rhyme across
   the page: 5 systemic forces here, 5 grassroots responses there. ---------- */
/* headline/lede sizes match every other section's pattern exactly:
   headline = clamp(2.6rem, 5vw, 5rem) (same as .s2-headline / .s3 h2 /
   .s4-flytext), lede = clamp(20px, 1.2vw, 3.5rem) (same as .s3 .section-head
   p). Centered here since s2b is a single column, not S3's two-column
   layout. */
.s2b-heading {
  font-family: var(--font-display);
  font-weight: 500;
  text-align: center;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  margin: 0 0 18px;
}

.s2b-lede {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: rgba(237, 227, 200, 0.82);
  max-width: 640px;
  margin: 0 auto 56px;
  text-align: center;
}

/* one bordered container, true 3-column grid with dividers between cells
   instead of 5 separate cards with gaps — modelled on histomatesea.org's
   stream-list section (its actual breakpoints: 3 cols above ~1150px,
   2 cols down to ~768px, 1 col below that — reproduced here at 1150px/
   640px, the latter matching this site's own mobile breakpoint everywhere
   else). 5 pillars in 3 columns lay out as 3 + 2, so a 6th "empty" filler
   cell (.pillar-card-empty, added in the markup) completes the rectangle
   exactly like the reference does for its own 5-of-6 grid. Assumes
   exactly 5 real pillar cards + 1 filler (6 total) — the nth-child border
   rules in every tier below are written for that fixed count. */
.pillar-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  max-width: 1320px;
  margin: 0 auto;
  background: rgba(237, 227, 200, 0.06);
  border: 1px solid rgba(237, 227, 200, 0.18);
  border-radius: 20px;
  overflow: hidden;
}

.pillar-card {
  padding: 30px 28px 28px;
  border-right: 1px solid rgba(237, 227, 200, 0.18);
  border-bottom: 1px solid rgba(237, 227, 200, 0.18);
  text-align: left;
}

/* 3-col tier (default, >1150px): last column of each row has no right
   divider, bottom row (cards 4-6) has no bottom divider. */
.pillar-card:nth-child(3n) {
  border-right: none;
}

.pillar-card:nth-child(n + 4) {
  border-bottom: none;
}

.pillar-card-empty {
  background: rgba(0, 0, 0, 0.1);
}

/* 2-col tier: 6 cells become 3 rows of 2, so the divider math changes —
   every even card loses its right divider, and only the last row (5-6)
   loses its bottom one. Re-declares the 3-col nth-child rules first since
   a max-width query below this one in the file can't out-specificity an
   nth-child selector above it otherwise. */
@media (max-width: 1150px) {
  .pillar-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .pillar-card:nth-child(3n) {
    border-right: 1px solid rgba(237, 227, 200, 0.18);
  }

  .pillar-card:nth-child(n + 4) {
    border-bottom: 1px solid rgba(237, 227, 200, 0.18);
  }

  .pillar-card:nth-child(2n) {
    border-right: none;
  }

  .pillar-card:nth-child(n + 5) {
    border-bottom: none;
  }
}

/* 1-col tier: everything stacks, so no right dividers at all and only
   the last visible card (5th — the empty 6th is hidden) loses its bottom
   divider. Same re-declare-then-override approach as the 2-col tier. */
@media (max-width: 640px) {
  .pillar-grid {
    grid-template-columns: 1fr;
  }

  .pillar-card,
  .pillar-card:nth-child(2n),
  .pillar-card:nth-child(3n) {
    border-right: none;
  }

  .pillar-card,
  .pillar-card:nth-child(n + 4),
  .pillar-card:nth-child(n + 5) {
    border-bottom: 1px solid rgba(237, 227, 200, 0.18);
  }

  .pillar-card:last-of-type {
    border-bottom: none;
  }

  .pillar-card-empty {
    display: none;
  }
}

.pillar-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(237, 227, 200, 0.6);
  margin-bottom: 16px;
}

.pillar-card h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.8rem;
  line-height: 1.35;
  color: var(--paper-text);
  margin: 0 0 10px;
}

.pillar-card p {
  font-size: 1rem;
  line-height: 1.6;
  color: rgba(237, 227, 200, 0.82);
  margin: 0;
}

.s2b-pivot {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  max-width: 640px;
  margin: 56px auto 0;
  text-align: center;
  color: var(--paper-text);
}

/* ---------- section 3 — practice grid (no effect, content-height) ---------- */
.s3 {
  position: relative;
  z-index: 0;
  padding: clamp(110px, 14vw, 190px) 0;
  background: var(--paper);
  color: var(--ink);
}

/* two columns: left is the headline, right is the globe. No .wrap class
   any more (was max-width:1120px, centered) — left edge now matches
   section 2's own text exactly instead: same 6vw padding, no centering,
   nothing capping the row's width on wide screens (section 2 doesn't
   cap either, so this keeps the two sections' left edges aligned at
   every viewport width, not just by coincidence at one). */
.s3-layout {
  display: flex;
  align-items: flex-start;
  gap: clamp(48px, 8vw, 110px);
  padding: 0 6vw;
}

.s3-left {
  /* flex-basis:0 (same as .s3-globe-wrap's) gives a straight 50/50 split
     of the row per request, instead of sizing off this column's own
     460px content basis. */
  flex: 1 1 0;
  /* without this, a flex item's automatic min-width is its content's
     min-content size — and since .s4-feed-track's cards are fixed-width,
     non-shrinking (flex:0 0 auto), that content size is the full
     unscrolled row (~1100px for 4 cards). That floor was winning over
     .s3-globe-wrap's own sizing at almost every real laptop width
     (confirmed from ~920px up to ~1366px), squeezing the globe to 0 —
     this is what was actually happening on a MacBook Air, not a one-off
     glitch. min-width:0 lets this column shrink below its content size,
     same fix the globe/ig-post flex-basis comments elsewhere in this
     file have needed for the analogous wrong-axis case. */
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.s3-left .s4-feed {
  margin-top: 28px;
}

/* .s4-post/-caption/-date and .s4-feed-more are styled in var(--paper-
   text)/var(--c-mustard) for section 4's navy background (see their base
   rules) — nested here in section 3's cream background instead, they
   need ink-family colors for the same reason .ig-post needed its own
   background when it briefly lived on navy: light-on-light vs.
   dark-on-dark is unreadable either way. */
.s3-left .s4-post {
  color: var(--ink);
}

.s3-left .s4-post-caption {
  color: var(--ink-soft);
}

.s3-left .s4-post-date {
  color: var(--ink-faint);
}

.s3-left .s4-post-media {
  background: rgba(31, 51, 80, 0.06);
}

.s3-left .s4-feed-track {
  scrollbar-color: rgba(31, 51, 80, 0.25) transparent;
}

.s3-left .s4-feed-track::-webkit-scrollbar-thumb {
  background: rgba(31, 51, 80, 0.2);
}

.s3 .section-head {
  margin: 0;
  text-align: left;
}

.s3 h2 {
  /* .s4-flytext (shared with section 4's headline) is centered now that
     section 4 is a single centered column — section 3's title keeps its
     own left alignment regardless. */
  text-align: left;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  margin-bottom: 14px;
}

.s3 .section-head p {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: var(--ink-soft);
}


/* below 900px .s3-layout carries .wrap's own padding:0 (every other
   section adds its own horizontal gutter on top of .wrap instead of
   relying on it alone — see .s2b-content's padding: 80px 6vw, same
   pattern), which read fine on desktop where the 1120px cap plus
   .wrap's auto margins already leaves generous breathing room, but on a
   narrow phone left content and the practice-card tile running flush to
   the screen edge with no gutter at all. */
@media (max-width: 900px) {
  .s3-layout {
    flex-direction: column;
    align-items: stretch;
    padding: 0 6vw;
  }
}

/* ---------- section 3 practice-card carousel — a UI experiment now
   nested inside .ig-post (see below), reusing Instagram's own
   carousel-post pattern (swipeable slides + dot indicators) for the same
   six practice cards, one at a time instead of all six in a wrapped
   grid. Each slide keeps its tag+description text inside the card itself
   (not pulled out as a caption) — this is a visual/interaction treatment
   of the existing content, not a new feature. Sizing is 100% of its
   parent .ig-post now rather than an independent flex/max-width, since it
   no longer sits directly in the .s3-layout row itself. ---------- */
.practice-carousel {
  width: 100%;
}

/* 16px side padding matches .ig-post-header's, so the tile's left edge lines
   up with the profile avatar and its right edge with the Follow button.
   The arrows sit at the very edge and overlap only the tile's empty
   padding zone, never its text. */
.practice-carousel-viewport {
  position: relative;
  padding: 0 16px;
}

/* all slides stack in the same grid cell (grid-area 1/1) so the container
   height tracks the tallest slide automatically; only opacity/pointer-
   events change between them, which crossfades cleanly with no layout
   jump and no display:none — sidesteps the [hidden]-vs-author-display
   cascade collision this project has hit twice before (see .story-card
   and .story-viewall history in earlier commits). */
.practice-carousel-track {
  display: grid;
}

.practice-carousel-slide {
  grid-area: 1 / 1;
  display: flex;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

.practice-carousel-slide.is-active {
  opacity: 1;
  pointer-events: auto;
}

.practice-carousel-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(250, 244, 199, 0.92);
  color: var(--c-navy);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  z-index: 2;
  transition: opacity 0.2s ease;
}

.practice-carousel-arrow:disabled {
  opacity: 0;
  pointer-events: none;
}

.practice-carousel-prev {
  left: 0;
}

.practice-carousel-next {
  right: 0;
}

.practice-carousel-arrow svg {
  width: 20px;
  height: 20px;
}

.practice-carousel-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin: 18px 0;
}

.practice-carousel-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  background: var(--line);
  cursor: pointer;
  padding: 0;
  transition: background 0.2s ease, width 0.2s ease, height 0.2s ease;
}

.practice-carousel-dot.is-active {
  background: var(--c-terracotta);
  width: 10px;
  height: 10px;
}

/* flat "photo" frame — deliberately no rotate/fold-shadow/paper-texture
   (that sticky-note treatment lived here before this UI experiment): now
   a plain square tile standing in for a real photo, so it reads as an
   Instagram image slot rather than a physical note. --note-color still
   tints each slide so the six practice ideas keep distinct identity
   until real photography replaces them. */
.practice-card {
  --note-color: var(--c-cream);
  --note-text: var(--c-navy);
  width: 100%;
}

.practice-card-inner {
  aspect-ratio: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 32px;
  text-align: center;
  color: var(--note-text);
  background-color: var(--note-color);
  border-radius: 16px;
}

.practice-tag {
  font-family: 'Shantell Sans', var(--font-display);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.2;
}

.practice-card p {
  margin: 0;
  font-family: 'Shantell Sans', var(--font-display);
  font-size: 16px;
  line-height: 1.5;
}

.practice-card.more .practice-card-inner {
  justify-content: center;
}

.practice-card.more a {
  color: inherit;
  font-weight: 700;
  text-decoration: none;
}

.practice-card.more a:hover {
  text-decoration: underline;
}

/* ---------- .ig-post — the section 3 practice carousel and the former
   "Eksperimen kecil" story-submission intro, combined into one mocked-up
   Instagram post card: profile header, carousel "photo", action row,
   stats+caption. Sits in section 4's .s4-row, next to the "Alter
   Imaginaries" text column. Avatar/photo are placeholders (initial
   letter, tinted squares) — this is a UI experiment ahead of real brand
   photography, not a functioning social account. ---------- */
.ig-post {
  flex: 2 1 380px;
  max-width: 460px;
  margin: 0 auto;
  /* explicit card background: this card's internals (username, follow
     pill, eyebrow, caption, action icons) are all set in --ink/--ink-
     faint, dark colors that only read correctly on a light surface. It
     used to sit directly on section 3's cream background with no card of
     its own, so this never mattered — now that it's sat on section 4's
     navy background at least once before, it needs its own light
     surface regardless of which section hosts it. */
  background: var(--paper);
  border-radius: 22px;
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3);
}

/* flex-basis resolves against the main axis, which switches from width to
   height once .s4-row goes column below 900px — without resetting it
   here, the 380px basis above clips .ig-post's own height to 380px
   instead of sizing its width (the exact bug hit earlier with the
   story-form name input's flex-basis). Placed after the base rule, not
   just inside the .s4-row media query below, so it actually wins the
   cascade at equal specificity — a later same-specificity rule beats an
   earlier one regardless of which is inside a matching media query. */
@media (max-width: 900px) {
  .ig-post {
    flex: none;
  }
}

.ig-post-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
}

.ig-post-avatar {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--c-navy);
}

.ig-post-who {
  flex: 1 1 auto;
  min-width: 0;
}

.ig-post-username {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 15px;
  color: var(--ink);
}

.ig-post-verified {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  color: #3897f0;
}

.ig-post-follow {
  flex: 0 0 auto;
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  background: rgba(0, 0, 0, 0.06);
  padding: 7px 14px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
}

.ig-post-follow:hover {
  background: rgba(0, 0, 0, 0.1);
}

.ig-post-caption {
  padding: 8px 16px 18px;
}

.ig-post-caption-eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 6px;
}

.ig-post-caption p {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--ink);
}

.ig-post .toast {
  padding: 0 16px 14px;
  margin-top: 0;
}

/* action bar — mimics an Instagram post's like/comment/share row. Like
   (post-level) is a decorative counter persisted to this browser only
   (see JS); the comment action opens the comments modal (view + compose);
   the share action opens a real external share sheet, unrelated to story
   submission. No dividing borders here since it now sits inside the
   .ig-post card between the photo and the caption, not as a standalone
   block on the page background. */
.story-actionbar {
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 12px 16px 6px;
  flex-wrap: wrap;
}

.story-action {
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: 19px;
  font-weight: 700;
  color: var(--ink-soft);
  cursor: pointer;
}

.story-action svg {
  width: 30px;
  height: 30px;
  stroke-width: 2.4;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.story-action.is-liked svg {
  fill: var(--c-terracotta);
  stroke: var(--c-terracotta);
  transform: scale(1.15);
}


/* prompt copy shown above the write-a-story form — the question + starter
   tips the site owner wants every submission guided by. */
.story-prompt {
  margin-bottom: 18px;
  padding: 16px 18px;
  background: var(--c-cream);
  border-radius: 16px;
}

.story-prompt-title {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--ink);
}

.story-prompt-question {
  margin: 0 0 12px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-soft);
}

.story-prompt-tips-label {
  margin: 0 0 4px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-faint);
}

.story-prompt-tips {
  margin: 0;
  padding-left: 18px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-soft);
}

/* mandatory photo picker — a dashed drop-zone-style button standing in
   for the <input type="file">, which stays in the DOM and focusable
   (accessibly hidden via clip, not display:none) so the <label> remains
   a real, keyboard-operable trigger for it. */
.story-photo-field {
  margin-bottom: 14px;
  position: relative;
}

.story-photo-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.story-photo-picker {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 18px;
  border: 1.5px dashed var(--line);
  border-radius: 16px;
  color: var(--ink-faint);
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.story-photo-picker:hover,
.story-photo-input:focus-visible + .story-photo-picker {
  border-color: var(--c-terracotta);
  color: var(--c-terracotta);
}

.story-photo-picker-icon svg {
  width: 22px;
  height: 22px;
}

.story-photo-picker-label {
  font-size: 14.5px;
  font-weight: 600;
}

.story-photo-preview {
  display: block;
  width: 100%;
  max-width: 260px;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 16px;
}

/* this project's own [hidden]-vs-author-display cascade collision again
   (see the practice-carousel-slide comment elsewhere in this file) —
   display:block above has equal specificity to the browser's own
   [hidden]{display:none} default and loads later, so it wins unless
   told otherwise here. */
.story-photo-preview[hidden] {
  display: none;
}

.story-photo-remove {
  position: absolute;
  top: 8px;
  left: 8px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: none;
  background: var(--ink);
  color: var(--paper-text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.story-photo-hint {
  margin: 6px 4px 0;
  font-size: 12.5px;
  color: var(--ink-faint);
}

.story-form {
  margin-bottom: 0;
}

.story-form textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  padding: 16px 18px;
  border: 1.5px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 18px;
  resize: vertical;
  min-height: 84px;
}

.story-form textarea:focus {
  outline: none;
  border-color: var(--c-terracotta);
}

/* name + email sit side by side and wrap to one per line when the modal is narrow.
   A plain row (never flex-direction: column), so the flex-basis stays a width. */
.story-form-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}

.story-form-fields input {
  flex: 1 1 160px;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 14.5px;
  padding: 11px 16px;
  border: 1.5px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 999px;
}

.story-form-fields input:focus {
  outline: none;
  border-color: var(--c-terracotta);
}

.story-privacy {
  margin: 8px 4px 0;
  font-size: 12.5px;
  color: var(--ink-faint);
}

.story-form-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.story-wordcount {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink-faint);
  white-space: nowrap;
}

.story-wordcount.out-of-range {
  color: var(--c-terracotta);
}

@media (max-width: 480px) {
  .story-form-row {
    flex-direction: column;
    align-items: stretch;
  }

  .story-form-row .btn {
    width: 100%;
    justify-content: center;
  }
}

.story-feed {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

/* HONY-style: photo on top (full card width), then the body — see
   .story-photo below. Column, not the old avatar+text row. */
.story-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  opacity: 1;
  transform: none;
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.story-card.story-card-enter {
  opacity: 0;
  transform: translateY(10px);
}

.story-photo {
  width: 100%;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: 18px;
  background: var(--c-cream);
}

.story-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* stand-in when a card has no imageUrl (see buildCard()'s icon-tile
   fallback) — a pending card replayed after a reload, whose local blob
   preview URL didn't survive, or a legacy story from before the photo
   requirement existed. Keeps every card the same shape instead of
   collapsing the photo slot. */
.story-photo.is-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-faint);
}

.story-photo.is-placeholder svg {
  width: 56px;
  height: 56px;
  stroke-width: 1.4;
}

.story-body {
  flex: 1 1 auto;
  min-width: 0;
}

/* blog-post headline — bigger and set in the serif display face, same
   family as every other heading on the site. Omitted (not shown as an
   empty element) for the handful of stories written before this field
   existed, same fallback pattern as a missing photo. */
.story-title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2vw, 1.9rem);
  line-height: 1.15;
  color: var(--ink);
}

/* author name on one side, like/share/flag actions on the other —
   replaces the old .story-head (name + badge only, actions sat in their
   own row below the body text). */
.story-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.story-meta-who {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}

.story-name {
  font-weight: 600;
  font-size: 16.5px;
}

.story-badge {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--c-terracotta);
  background: rgba(193, 84, 63, 0.1);
  padding: 3px 9px;
  border-radius: 999px;
  margin-left: 8px;
}

.story-text {
  margin: 0;
  font-size: 16.5px;
  line-height: 1.55;
  color: var(--ink);
}

/* per-story like — decorative only, same as the post-level action bar
   like: local click state, not persisted, resets on reload. */
.story-like {
  display: flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--ink-faint);
  cursor: pointer;
}

.story-like svg {
  width: 18px;
  height: 18px;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.story-like.is-liked {
  color: var(--c-terracotta);
}

.story-like.is-liked svg {
  fill: var(--c-terracotta);
  stroke: var(--c-terracotta);
  transform: scale(1.15);
}

/* sits in .story-meta, opposite the author's name — like, share, and (for
   comments that come from the server) the report/flag button. */
.story-card-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

.story-share {
  display: flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--ink-faint);
  cursor: pointer;
}

.story-share svg {
  width: 17px;
  height: 17px;
}

.story-share:hover {
  color: var(--c-terracotta);
}

.story-flag {
  display: flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--ink-faint);
  cursor: pointer;
}

.story-flag svg {
  width: 16px;
  height: 16px;
}

.story-flag:hover {
  color: var(--c-terracotta);
}

.story-flag.is-flagged {
  color: var(--c-terracotta);
  opacity: 0.55;
  cursor: default;
}

/* honeypot field: invisible to people, tempting to form-filling bots */
.story-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.story-action:disabled {
  opacity: 0.6;
  cursor: wait;
}

/* ---------- shared modal shell — used by both the comments modal and the
   share modal. Hidden via opacity+pointer-events (not display:none) so
   the open/close transition can animate. ---------- */
.story-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.story-modal.is-open {
  opacity: 1;
  pointer-events: auto;
}

.story-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 16, 12, 0.55);
}

.story-modal-panel {
  position: relative;
  width: min(92vw, 560px);
  max-height: 86vh;
  overflow-y: auto;
  background: var(--paper);
  color: var(--ink);
  border-radius: 22px;
  padding: clamp(28px, 4vw, 36px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
  transform: translateY(14px) scale(0.97);
  transition: transform 0.25s ease;
}

.story-modal.is-open .story-modal-panel {
  transform: none;
}

.story-modal-close {
  position: absolute;
  top: 14px;
  right: 16px;
  background: none;
  border: none;
  font-size: 26px;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 4px 8px;
}

.story-modal-close:hover {
  color: var(--ink);
}

.story-modal-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.6rem, 2.6vw, 2rem);
  margin: 0 0 20px;
  padding-right: 24px;
}

body.story-modal-locked {
  overflow: hidden;
}

/* comments modal — the feed scrolls in its own area while the compose
   form stays put at the bottom, like a native comment sheet. */
.story-comments-panel {
  display: flex;
  flex-direction: column;
  overflow-y: hidden;
}

.story-comments-panel .story-feed {
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0 0 20px;
  padding-right: 4px;
}

.story-comments-panel .story-form {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
  padding-top: 18px;
}

/* share modal — real external share targets, icon + label grid like
   Instagram's own share sheet. */
.share-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 20px 12px;
  margin-top: 4px;
}

.share-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  color: var(--ink);
  font-size: 14px;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
}

.share-option-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--paper-text);
  font-size: 22px;
  font-weight: 700;
  font-family: var(--font-display);
}

.share-option-icon svg {
  width: 26px;
  height: 26px;
}

.share-option[hidden] {
  display: none;
}

/* zine section layout — cover and description sit side by side on desktop
   (see the 901px+ override below), stacked (cover on top) on narrower
   screens. Section 6 now reads as a full standalone section (its own
   heading scale, no boxed-card treatment), not a small promo card. */
.s6-zine-layout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;
  margin: 0 auto;
}

/* capped separately from the shared .wrap max-width (88vw), and only above
   the viewport where 88vw would otherwise exceed 980px (980 / 0.88 ≈
   1114px) — setting this unconditionally overrode .wrap's own max-width at
   narrower widths (same specificity, declared later), which was
   collapsing this section's side gutter on mobile. */
@media (min-width: 1114px) {
  .s6-zine-layout {
    max-width: 980px;
  }
}

@media (min-width: 901px) {
  .s6-zine-layout {
    flex-direction: row;
    align-items: center;
    text-align: left;
  }
}

/* 3D book-tilt cover, ported verbatim from a CodePen (3D Book Flip Cover,
   https://codepen.io/realvjy/pen/KKEdXOa) — only the image swapped in.
   Class names and values kept exactly as the source authored them. */
.s3-zine-cover {
  flex: 0 0 auto;
  max-width: 240px;
  position: relative;
  padding: 16px;
}

@media (min-width: 901px) {
  .s3-zine-cover {
    max-width: 400px;
  }
}

.s3-zine-cover .main-book-wrap {
  position: relative;
}

.s3-zine-cover .book-cover {
  position: relative;
}

.s3-zine-cover .book-cover .book-inside {
  position: absolute;
  width: 90%;
  height: 96%;
  top: 1%;
  left: 16px;
  border: 1px solid grey;
  border-radius: 2px 6px 6px 2px;
  background: white;
  box-shadow:
    10px 40px 40px -10px #00000030,
    inset -2px 0 0 grey,
    inset -3px 0 0 #dbdbdb,
    inset -4px 0 0 white,
    inset -5px 0 0 #dbdbdb,
    inset -6px 0 0 white,
    inset -7px 0 0 #dbdbdb,
    inset -8px 0 0 white,
    inset -9px 0 0 #dbdbdb;
}

.s3-zine-cover .book-cover .book-image {
  line-height: 0;
  position: relative;
  border-radius: 2px 6px 6px 2px;
  box-shadow:
    6px 6px 18px -2px rgba(0, 0, 0, 0.2),
    24px 28px 40px -6px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease-in-out;
  transform: perspective(2000px) rotateY(-15deg) translateX(-10px) scaleX(0.94);
  cursor: pointer;
}

.s3-zine-cover .book-image img {
  width: 100%;
  border-radius: 2px 6px 6px 2px;
  display: block;
}

.s3-zine-cover .book-image:hover {
  transform: perspective(2000px) rotateY(0deg) translateX(0px) scaleX(1);
  transform-style: preserve-3d;
  box-shadow:
    6px 6px 12px -1px rgba(0, 0, 0, 0.1),
    20px 14px 16px -6px rgba(0, 0, 0, 0.1);
}

.s3-zine-cover .effect {
  position: absolute;
  width: 20px;
  height: 100%;
  margin-left: 7px;
  top: 0;
  border-left: 2px solid #00000010;
  background-image: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
  transition: all 0.5s ease;
  z-index: 5;
}

.s3-zine-cover .light {
  width: 90%;
  height: 100%;
  position: absolute;
  border-radius: 3px;
  background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.2) 100%);
  top: 0;
  right: 0;
  opacity: 0.1;
  transition: all 0.5s ease;
  z-index: 4;
}

.s3-zine-cover .book-image:hover .effect {
  margin-left: 7px;
}

.s3-zine-body {
  flex: 0 1 auto;
  text-align: center;
}

@media (min-width: 901px) {
  .s3-zine-body {
    text-align: left;
  }
}

/* heading/lede sizes match every other section's pattern: heading =
   clamp(2.6rem, 5vw, 5rem), lede = clamp(20px, 1.2vw, 3.5rem) (see .s2b,
   .s3, .s4). The real zine title lives in the small eyebrow above the
   heading instead of being the heading itself, since the heading here is
   a hook line, not the publication's actual name. */
.s6-zine-eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}

.s6-zine-heading {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  margin: 0 0 18px;
}

.s6-zine-lede {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: var(--ink-soft);
  line-height: 1.6;
  margin: 0 0 28px;
}

/* ---------- section 6 — dedicated, full-weight zine section (was a small
   promo card shared with section 3). Same paper background + plaster
   texture as before, just given the same heading scale as every other
   section instead of reading as a lightweight aside. ---------- */
.s6-zine {
  position: relative;
  z-index: 0;
  padding: clamp(70px, 9vw, 110px) 0;
  background: var(--paper);
  color: var(--ink);
}

.s6-zine::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: url('../assets/textures/wall-plaster-texture.jpg');
  background-size: cover;
  background-position: center;
  opacity: 0.45;
  pointer-events: none;
}

/* ---------- section 4 — pointer-driven "Alter Imaginaries" + proof ---------- */
.s4 {
  position: relative;
  background: var(--c-navy);
  color: var(--paper-text);
  padding: clamp(70px, 9vw, 110px) 0;
  overflow: hidden;
}

/* ---------- section 5 — subscribe, moved out of section 4 to its own
   standalone section below the zine. Keeps the same navy treatment
   section 4 had, since .s5-subscribe's own styles (paper-text copy,
   paper-fill button) assume a dark background regardless of which
   section hosts it. ---------- */
.s5 {
  position: relative;
  background: var(--c-navy);
  color: var(--paper-text);
  padding: clamp(70px, 9vw, 110px) 0;
}

.s4-cta-block {
  text-align: center;
}

/* outer column: the text+ig-post row, full width. No .wrap class any
   more, same reasoning as .s3-layout above — 6vw padding, no centering,
   so this lines up with sections 2 and 3 at every viewport width. */
.s4-layout {
  display: flex;
  flex-direction: column;
  gap: 56px;
  padding: 0 6vw;
}

/* the actual two-column row: text left, ig-post right — same pattern as
   .s3-layout. */
.s4-row {
  display: flex;
  align-items: flex-start;
  gap: clamp(48px, 8vw, 110px);
  text-align: left;
}

@media (max-width: 900px) {
  .s4-row {
    flex-direction: column;
  }
}

.s4-text-col {
  flex: 1 1 460px;
  max-width: 920px;
}

.s4-text-col .gooey-wrap {
  margin-top: 28px;
}

/* .btn-solid defaults to an ink fill (var(--ink), which on this page
   equals var(--c-navy)) — on section 4's own navy background that's a
   navy-on-navy button with no visible fill, only its text readable. Same
   fix as .s1/.s5-subscribe's own .btn-solid overrides: an orange fill
   that actually shows up against the dark background. */
#s3SendStoryBtn {
  background: var(--c-terracotta);
  color: var(--paper-text);
  border-color: var(--c-terracotta);
}

#s3SendStoryBtn:hover {
  background: var(--paper-text);
  color: var(--c-terracotta);
  border-color: var(--paper-text);
}

/* name is legacy — this used to also carry the per-character wind-blown
   animation hook (see the removed .fly-text engine); now it's just the
   shared headline size/weight for s3 and s4, animated with a plain
   fade/rise like every other section instead. Left-aligned by default —
   section 3's own h2 already forced this independently (.s3 h2), section
   4's now matches without needing its own override. */
.s4-flytext {
  font-family: var(--font-display);
  font-weight: 400;
  text-align: left;
  font-size: clamp(2.6rem, 5vw, 6rem);
  line-height: 1.05;
  margin: 0 0 28px;
}

.s4 .body-copy {
  text-align: left;
  margin: 0;
}

.s4 .body-copy p {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: var(--paper-text);
  margin: 0 0 12px;
}

/* ---------- Instagram strip (now in section 3) — the latest
   @alterimaginaries posts (max 4, filled in by the section script from
   the Vercel API): a square photo, date and caption per card, scrolled
   sideways. Replaces the three scattered polaroids and their lightbox
   that used to live here. ---------- */
.s4-feed {
  width: 100%;
}

.s4-feed-scroller {
  position: relative;
}

.s4-feed-track {
  display: flex;
  gap: 18px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding: 4px 2px 20px;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: rgba(237, 227, 200, 0.35) transparent;
}

.s4-feed-track::-webkit-scrollbar {
  height: 6px;
}

.s4-feed-track::-webkit-scrollbar-thumb {
  background: rgba(237, 227, 200, 0.3);
  border-radius: 999px;
}

.s4-feed-track:focus-visible {
  outline: 2px solid var(--c-mustard);
  outline-offset: 4px;
  border-radius: 12px;
}

.s4-post {
  flex: 0 0 auto;
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  scroll-snap-align: start;
  color: var(--paper-text);
  text-decoration: none;
}

.s4-post-media {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 16px;
  overflow: hidden;
  background: rgba(237, 227, 200, 0.1);
}

.s4-post-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s ease;
}

a.s4-post:hover .s4-post-media img {
  transform: scale(1.04);
}

.s4-post-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgba(20, 30, 50, 0.6);
  color: var(--paper-text);
  display: flex;
  align-items: center;
  justify-content: center;
}

.s4-post-badge svg {
  width: 15px;
  height: 15px;
}

.s4-post-date {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-mustard);
}

.s4-post-caption {
  margin: 0;
  font-size: 15px;
  line-height: 1.5;
  color: rgba(237, 227, 200, 0.9);
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* loading placeholders */
.s4-post--skeleton .s4-post-media,
.s4-post-line {
  animation: s4-pulse 1.4s ease-in-out infinite;
}

.s4-post-line {
  height: 12px;
  border-radius: 6px;
  background: rgba(237, 227, 200, 0.1);
}

.s4-post-line--short {
  width: 60%;
}

@keyframes s4-pulse {
  50% {
    opacity: 0.45;
  }
}

.s4-feed-arrow {
  position: absolute;
  /* half of a card's square photo, so the arrows sit on the images, not the captions */
  top: 130px;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(250, 244, 199, 0.92);
  color: var(--c-navy);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  z-index: 2;
  transition: opacity 0.2s ease;
}

.s4-feed-arrow svg {
  width: 20px;
  height: 20px;
}

.s4-feed-arrow:disabled {
  opacity: 0;
  pointer-events: none;
}

.s4-feed-prev {
  left: -6px;
}

.s4-feed-next {
  right: -6px;
}

/* touch devices swipe; the arrows are for mouse users */
@media (hover: none) {
  .s4-feed-arrow {
    display: none;
  }
}

/* feed missing or empty: hide the strip, keep only the Instagram link */
.s4-feed[data-state="error"] .s4-feed-scroller {
  display: none;
}

/* plain .btn (same outline treatment as "Ikuti Instagram kami" in the
   subscribe section) — color/border-color inherit from whatever section
   hosts it, same as every other outline .btn on the page. */
.s4-feed-more {
  margin-top: 20px;
}

@media (max-width: 700px) {
  .s4-post {
    width: 72vw;
    max-width: 260px;
  }
}

/* ---------- text-scramble — the statement itself. Lives on about.html
   now (below Komunitas, #about-scramble), left-aligned there via the
   --left modifier; the shared base rule stays centered for any other
   use. ---------- */
.s5-scramble {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.4rem, 6vw, 2.2rem);
  line-height: 1.3;
  max-width: 100%;
  margin: 0 auto 48px;
  min-height: 1.3em;
  display: flex;
  align-items: center;
  justify-content: center;
}

.s5-scramble--left {
  justify-content: flex-start;
  margin: 0;
}

/* one line per phrase — only once the viewport is wide enough that the
   longest phrase (~54 characters) actually fits at a readable size */
@media (min-width: 720px) {
  .s5-scramble {
    font-size: clamp(1.1rem, 3vw, 3rem);
    white-space: nowrap;
  }
}

.s5-scramble .dud {
  opacity: 0.5;
}

.s5-subscribe {
  margin: 64px auto 0;
  max-width: 760px;
}

/* heading/lede sizes match every other section's pattern: heading =
   clamp(2.6rem, 5vw, 5rem), lede = clamp(20px, 1.2vw, 3.5rem) (see .s2b,
   .s3, .s4, .s6-zine). */
.s5-subscribe h3 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  margin: 0 0 18px;
}

.s5-subscribe-sub {
  font-size: clamp(20px, 1.2vw, 3.5rem);
  color: rgba(237, 227, 200, 0.82);
  margin: 0 0 32px;
}

.s5-subscribe .subform {
  justify-content: center;
}

.s5-subscribe .subform input {
  flex: 1 1 auto;
  border-radius: 999px;
  padding-left: 20px;
  /* 16px minimum: below this, iOS Safari auto-zooms the page on focus. */
  font-size: 16px;
}

/* Subscribe + Follow-on-Instagram stay paired side by side, even if the
   email input wraps to its own line first on narrower widths. */
.subform-buttons {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

@media (min-width: 480px) {
  .subform-buttons {
    flex-wrap: nowrap;
  }
}

@media (max-width: 480px) {
  .subform-buttons .btn {
    width: 100%;
    justify-content: center;
  }
}

.s5-subscribe .toast {
  text-align: center;
}

/* .btn-solid defaults to an ink fill, made for paper-background sections.
   On this card's dark navy it would read as near-black-on-navy (low
   contrast, doesn't pop) so it gets the same paper-fill treatment as the
   hero's own dark-section buttons (see .s1 .btn-solid). */
.s5-subscribe .btn-solid {
  background: var(--paper-text);
  color: var(--c-navy);
  border-color: var(--paper-text);
}

.s5-subscribe .btn-solid:hover {
  background: var(--c-terracotta);
  border-color: var(--c-terracotta);
  color: var(--paper-text);
}

/* ---------- footer ---------- */
.sitefoot {
  padding: 34px 0;
  background: var(--c-navy);
  color: rgba(237, 227, 200, 0.6);
}

.sitefoot .wrap {
  font-size: 12.5px;
}

.sitefoot .footbottom {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  border-top: none;
  padding-top: 0;
}

.sitefoot .footcredits {
  border-top: none;
}

/* ---------- cerita.html / en/cerita.html — standalone page listing every
   approved story as a grid of the same HONY-style .story-card used in the
   homepage's "Kirim ceritamu" modal (see .story-card etc. above), instead
   of that modal's single-column scrolling feed. Not loaded with
   body.page-home, so the header keeps styles.css's default sticky
   solid-background treatment rather than the homepage's transparent
   floating one. ---------- */
.stories-hero {
  background: var(--paper);
  color: var(--ink);
  padding: clamp(60px, 8vw, 100px) 0 clamp(36px, 5vw, 56px);
}

.stories-hero-text {
  max-width: 720px;
}

.stories-heading {
  margin: 0 0 18px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 1.05;
  text-align: left;
}

.stories-lede {
  margin: 0 0 28px;
  font-size: clamp(18px, 1.2vw, 24px);
  line-height: 1.5;
  color: var(--ink-soft);
}

.stories-list-section {
  padding: 0 0 clamp(70px, 9vw, 110px);
}

.stories-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 32px;
  position: relative;
  min-height: 120px;
}

.stories-state {
  grid-column: 1 / -1;
  padding: 48px 0;
  text-align: center;
  font-size: 16px;
  color: var(--ink-faint);
}

/* small floating confirmation (flag reports, like/flag errors) — the
   homepage's own .toast is designed to sit inline inside a specific
   form/card, not float; this page has no such container to nest one in,
   so it gets a simple fixed, self-dismissing banner instead. */
.stories-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%) translateY(12px);
  z-index: 300;
  max-width: calc(100vw - 32px);
  padding: 12px 20px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 14px;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.stories-toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
/* ---------- cerita.html / en/cerita.html: grid cards, quote cards and the
   story detail view (2026-10-11). Everything here is scoped to .stories-grid
   or to the sd-* classes, because .story-card / .story-photo / .story-title
   are shared with the homepage's "Kirim ceritamu" modal feed and must not
   change there. Grid: every card has the same height (set from
   js/stories.js as --grid-card-h), square thumbnails, one excerpt size, and
   titles and names in the same small mono style. ---------- */
.stories-grid .story-card {
  position: relative;
  height: var(--grid-card-h, auto);
  align-self: start;
}

.stories-grid .story-photo {
  aspect-ratio: 1 / 1;
}

.stories-grid .story-photo img {
  transition: transform 0.5s ease;
}

.stories-grid .story-card:hover .story-photo img {
  transform: scale(1.03);
}

.stories-grid .story-title {
  margin: 0 0 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 1.55;
  text-transform: uppercase;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.stories-grid .story-text {
  font-family: var(--font-display);
  font-size: 19px;
  line-height: 1.4;
}

.stories-grid .story-card:not(.is-quote) .story-text.is-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  overflow: hidden;
  min-height: calc(4 * 1.4em);
}

/* name + like/share/flag pinned to the bottom of every card; a long name is
   cut with an ellipsis so it stays on one line beside the buttons */
.stories-grid .story-foot {
  flex-wrap: nowrap;
  margin: 6px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.stories-grid .story-meta-who {
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}

.stories-grid .story-name {
  display: block;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stories-grid .story-card-actions {
  flex-shrink: 0;
}

/* the whole card opens the story; the like/share/flag buttons sit above the link */
.card-link {
  color: inherit;
  text-decoration: none;
}

.card-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 18px;
}

.card-link:focus-visible {
  outline: none;
}

.card-link:focus-visible::after {
  outline: 2px solid var(--c-terracotta);
  outline-offset: 4px;
}

.stories-grid .story-card:not(.is-quote):hover .story-title .card-link {
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 4px;
}

.stories-grid .story-card-actions {
  position: relative;
  z-index: 2;
}

/* quote card: stands in for the photo when a story has none */
.stories-grid .story-card.is-quote {
  justify-content: space-between;
  gap: 18px;
  padding: clamp(22px, 3vw, 30px);
  border-radius: 18px;
  border: 1px solid transparent;
  transition: box-shadow 0.3s ease;
}

.stories-grid .story-card.is-quote:hover {
  box-shadow: 0 10px 24px -14px rgba(31, 51, 80, 0.45);
}

.story-card.is-quote.sq-terracotta { background: var(--c-terracotta); color: var(--paper-text); }
.story-card.is-quote.sq-navy { background: var(--c-navy); color: var(--paper-text); }
.story-card.is-quote.sq-mustard { background: var(--c-mustard); color: var(--c-navy); }
.story-card.is-quote.sq-teal { background: var(--c-teal); color: var(--paper-text); }
.story-card.is-quote.sq-plum { background: var(--c-plum); color: var(--paper-text); }
.story-card.is-quote.sq-cream { background: var(--c-cream); color: var(--ink); border-color: var(--line); }

.story-card.is-quote .quote-mark {
  display: block;
  font-family: var(--font-display);
  font-size: 3rem;
  line-height: 0.7;
  height: 0.5em;
  opacity: 0.55;
}

.story-card.is-quote .quote-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.stories-grid .story-card.is-quote .story-title {
  margin: 0;
  color: inherit;
}

/* the paragraph takes all the room left in the box; js/stories.js sets the
   line count so the ellipsis lands on the last line that fits */
.stories-grid .story-card.is-quote .story-text.is-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  flex: 1 1 0;
  min-height: 0;
  color: inherit;
  text-wrap: pretty;
}

.story-card.is-quote .story-foot {
  margin: 0;
  border-top-color: color-mix(in srgb, currentColor 30%, transparent);
}

.story-card.is-quote .story-like,
.story-card.is-quote .story-share,
.story-card.is-quote .story-flag {
  color: inherit;
  opacity: 0.85;
}

.story-card.is-quote .story-like:hover,
.story-card.is-quote .story-share:hover,
.story-card.is-quote .story-flag:hover,
.story-card.is-quote .story-like.is-liked {
  color: inherit;
  opacity: 1;
}

.story-card.is-quote .story-like.is-liked svg {
  fill: currentColor;
  stroke: currentColor;
}

.stories-grid .story-card button:focus-visible,
.sd-meta button:focus-visible {
  outline: 2px solid var(--c-terracotta);
  outline-offset: 3px;
  border-radius: 4px;
}

.stories-grid .story-card.is-quote button:focus-visible {
  outline-color: currentColor;
}

/* ---- story detail view ---- */
.sd-hero {
  padding: clamp(36px, 5vw, 64px) 0 clamp(28px, 4vw, 44px);
}

.sd-col {
  max-width: 720px;
  margin: 0 auto;
}

.sd-back {
  margin-bottom: clamp(28px, 4vw, 48px);
}

.sd-title {
  margin: 0 0 clamp(22px, 3vw, 32px);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(2rem, 4.4vw, 3.5rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.sd-title:focus {
  outline: none;
}

.sd-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  flex-wrap: wrap;
  padding-block: 14px;
  border-block: 1px solid var(--line);
}

.sd-who {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 12px;
  min-width: 0;
}

.sd-sub {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink-faint);
}

.sd-figure {
  margin: clamp(24px, 3.5vw, 40px) auto 0;
  padding: 0;
}

.sd-photo {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  max-height: 74vh;
  width: auto;
  height: auto;
  border-radius: 18px;
  background: var(--c-cream);
}

.sd-body {
  padding: clamp(32px, 4.5vw, 56px) 0 clamp(48px, 7vw, 80px);
}

.sd-body p {
  margin: 0 0 1.25em;
  max-width: 65ch;
  font-family: var(--font-display);
  font-size: clamp(19.5px, 1.55vw, 22px);
  line-height: 1.55;
  color: var(--ink);
}

.sd-body p:first-child {
  font-size: clamp(21px, 1.85vw, 25px);
  line-height: 1.45;
}

.sd-body p:last-child {
  margin-bottom: 0;
}

.sd-more {
  padding: clamp(44px, 6vw, 72px) 0 clamp(60px, 8vw, 96px);
  border-top: 1px solid var(--line-soft);
}

.sd-more-title {
  margin: 0 0 28px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.2vw, 3rem);
  line-height: 1.1;
}

.sd-more .stories-grid {
  min-height: 0;
}

.sd-cta {
  background: var(--c-teal);
  color: var(--paper-text);
  padding: clamp(52px, 7vw, 88px) 0;
}

.sd-cta h2 {
  margin: 0 0 14px;
  max-width: 18ch;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 3.6vw, 3.4rem);
  line-height: 1.08;
  text-wrap: balance;
}

.sd-cta p {
  margin: 0 0 28px;
  max-width: 520px;
  font-size: clamp(17px, 1.2vw, 21px);
  line-height: 1.5;
}

.sd-cta .btn {
  background: var(--paper-text);
  color: var(--c-navy);
  border-color: var(--paper-text);
}

.sd-cta .btn:hover {
  background: var(--c-terracotta);
  border-color: var(--c-terracotta);
  color: var(--paper-text);
}

@media (prefers-reduced-motion: reduce) {
  .stories-grid .story-photo img,
  .stories-grid .story-card.is-quote {
    transition: none;
  }

  .stories-grid .story-card:hover .story-photo img {
    transform: none;
  }
}

/* ---------- homepage "Kirim ceritamu" modal feed: a story without a photo is
   a coloured quote card, same look as on the stories page (the colour rules
   .story-card.is-quote.sq-* above are shared). Natural height here: the
   feed is a single scrolling column, not an equal-height grid. ---------- */
.story-feed .story-card.is-quote {
  gap: 18px;
  padding: clamp(22px, 4vw, 30px);
  border-radius: 18px;
  border: 1px solid transparent;
}

.story-feed .story-card.is-quote.sq-cream {
  border-color: var(--line);
}

.story-feed .story-card.is-quote .story-title {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 1.55;
  text-transform: uppercase;
  color: inherit;
}

.story-feed .story-card.is-quote .story-text {
  font-family: var(--font-display);
  font-size: 19px;
  line-height: 1.4;
  color: inherit;
  text-wrap: pretty;
}

.story-feed .story-card.is-quote .story-foot {
  margin: 0;
  padding-top: 14px;
  border-top: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}

.story-feed .story-card.is-quote .story-name {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.story-card.is-quote .story-badge {
  color: inherit;
  background: color-mix(in srgb, currentColor 18%, transparent);
}
