/* src/styles/case-figures.css */
/* Case-study gallery — the `/proekti/:slug` body outlet.
 *
 * Ships as a code asset, not editor CSS: body content is markdown-rendered
 * at publish time, so its elements are not page-tree nodes and editor class
 * rules for them would not survive the publish tree-shake. Interactive state
 * (video pointer-events, the compare handle and grip) is set inline from
 * motion.js — only static base styles live here.
 */

.case-figures p {
  margin: 0;
}

.case-figures img,
.case-figures video {
  display: block;
  width: 100%;
  /* Gallery media is presentation, not content to drag or select. Videos
   * keep pointer events here — JS sets pointer-events: none inline only when
   * it strips the controls, so the no-JS / reduced-motion state stays usable. */
  user-select: none;
  -webkit-user-drag: none;
}

.case-figures iframe {
  display: block;
  width: 100%;
  /* Vimeo/YouTube embeds arrive as raw width="640" height="360" attributes
   * from the source markdown — without this the iframe renders at that fixed
   * size instead of filling the column like every other gallery item, which
   * reads as a blank gap around it. aspect-ratio + height:auto lets a
   * replaced element scale responsively off its authored 16:9 dimensions. */
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
}

.img-compare:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: -2px;
}

/* Project-card "Виж <project>" button — /proekti, /en/work and every
 * service page's featured project. The label inherits `.button-main__label`'s
 * nowrap, so a long client name ("Виж Arcline Architecture") pushed phone
 * layouts sideways. Lives here because the editor drops descendant rules
 * at publish; `.work-entry__more` itself wraps via its editor class rule. */
@media (max-width: 900px) {
  .work-entry__more .button-main__label {
    text-wrap-mode: wrap;
    text-align: center;
  }
}

/* Service-page "Запази разговор" card — the button and the inquiry link
 * overflowed the card on phones. Mirrors `.button-main__inner--fluid`;
 * `.service-next__button` (width: 100%) and the card padding stay as
 * editor class rules. Lives here for the same descendant-rule reason. */
@media (max-width: 440px) {
  .service-next__button .button-main__inner {
    flex-grow: 1;
    padding-inline: var(--spacing-base);
  }

  .service-next__button .button-main__label {
    text-wrap-mode: wrap;
    text-align: center;
  }

  .service-next .case-meta__story {
    text-wrap-mode: wrap;
  }
}

/* Service-page meta strip reviews — stars + review count, then the cited
 * clients' portraits with one name. Both numbers are CSS counters over the
 * loops' rendered items (`.service-reviews` resets `reviews` and
 * `people -1`): Instatic exposes no loop-total token, and counting in CSS
 * keeps the strip server-rendered. Both people loops order portraits first,
 * so the name (a second loop, limit 1) belongs to the front face. Pseudo and
 * sibling rules live here because the publisher drops them from editor CSS. */
.service-rating__number::before {
  content: counter(reviews);
}

.service-people__count::before {
  content: counter(people);
}

.service-people__face:not(:first-child) {
  margin-left: calc(-1 * var(--spacing-md));
}

/* The first face sits on top. */
.service-people__face:nth-child(1) {
  z-index: 3;
}

.service-people__face:nth-child(2) {
  z-index: 2;
}

.service-reviews:hover .service-rating__count,
.service-reviews:hover .service-people__text {
  text-decoration: underline;
}

/* Link underlines — one line, one motion, two resting states.
 *
 * Standalone links (arrow, on their own line: case-study breadcrumb,
 * services list, meta-strip story links) carry no line at rest; hovering the
 * link draws it in under the label from the left, and it leaves to the right.
 * In-text links (no arrow, inside a sentence or the footer legal row) are
 * underlined at rest; hover slides the line out to the right and redraws it
 * from the left. The line is a currentColor gradient rather than
 * text-decoration so it can sweep. On standalone links it sits on the label,
 * not the link, so it never runs under the arrow.
 *
 * `.announce__link` mirrors the in-text half inside the embrand.announcement
 * plugin. Lives here because the publisher drops pseudo and descendant rules
 * from editor CSS. */
.case-meta__story-label,
.case-breadcrumb__label,
.case-services__label,
.footer-form__consent-link,
.footer-legal__links a {
  text-decoration-line: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 100% 100%;
  background-size: 0% 1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size 250ms ease-out;
}

.case-meta__story:is(:hover, :focus-visible) .case-meta__story-label,
.case-breadcrumb__link:is(:hover, :focus-visible) .case-breadcrumb__label,
.case-services__link:is(:hover, :focus-visible) .case-services__label {
  background-position: 0 100%;
  background-size: 100% 1px;
}

.footer-form__consent-link,
.footer-legal__links a {
  background-position: 0 100%;
  background-size: 100% 1px;
}

.footer-form__consent-link:is(:hover, :focus-visible),
.footer-legal__links a:is(:hover, :focus-visible) {
  animation: link-line-redraw 500ms ease-out;
}

@keyframes link-line-redraw {
  0% {
    background-position: 100% 100%;
    background-size: 100% 1px;
  }

  50% {
    background-position: 100% 100%;
    background-size: 0% 1px;
  }

  50.01% {
    background-position: 0 100%;
    background-size: 0% 1px;
  }

  100% {
    background-position: 0 100%;
    background-size: 100% 1px;
  }
}

/* Reduced motion: no sweep. Standalone lines appear at once; in-text lines
 * thicken to 2px instead of redrawing. */
@media (prefers-reduced-motion: reduce) {
  .case-meta__story-label,
  .case-breadcrumb__label,
  .case-services__label,
  .footer-form__consent-link,
  .footer-legal__links a {
    transition: none;
  }

  .footer-form__consent-link:is(:hover, :focus-visible),
  .footer-legal__links a:is(:hover, :focus-visible) {
    animation: none;
    background-size: 100% 2px;
  }
}

/* Footer contact pills (email, Telegram, phone) — the pill version of the
 * in-text links: a thin stroke at rest, and on hover a 2px stroke draws
 * itself around the pill clockwise from the left end, un-drawing on leave.
 * The ring is a ::after over the existing border (no layout shift), revealed
 * by a conic mask whose angle animates through a registered property;
 * browsers without @property show the full ring at once. The address line
 * (.footer-nap__line-nostroke) has no pill and is left out. While the email
 * pill shows its green "copied" state (motion.js sets data-state="copy"),
 * the ring stays hidden so it never covers the green border. */
@property --pill-sweep {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.footer-nap__line:not(.footer-nap__line-nostroke) {
  position: relative;
}

.footer-nap__line:not(.footer-nap__line-nostroke)::after {
  content: "";
  position: absolute;
  /* Whole pixels on purpose: the pill's 0.5px border renders as 1px at 1x
   * density, so a fractional inset or stroke lands unevenly around the pill. */
  inset: -1px;
  border: 2px solid currentColor;
  border-radius: inherit;
  pointer-events: none;
  -webkit-mask-image: conic-gradient(from 270deg, #000 var(--pill-sweep), transparent 0);
  mask-image: conic-gradient(from 270deg, #000 var(--pill-sweep), transparent 0);
  transition: --pill-sweep 400ms ease-out;
}

.footer-nap__line:not(.footer-nap__line-nostroke):is(:hover, :focus-visible)::after {
  --pill-sweep: 360deg;
}

.footer-nap__line[data-state="copy"]::after {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .footer-nap__line:not(.footer-nap__line-nostroke)::after {
    transition: none;
  }
}


/* src/styles/hero-fixed-media.css */
/**
 * Embrand Studio — hero media fixed to the viewport.
 *
 * The hero image or video stays put while its box scrolls up over it, the
 * same read as `background-attachment: fixed`.
 *
 * The media is plain `position: fixed`, and the hero clips it with
 * `clip-path`. `overflow: hidden` does not clip fixed descendants;
 * `clip-path` does, in every engine. No scroll-linked animation: the
 * browser composites the fixed layer, so it is the cheapest version of the
 * effect and it cannot drift from the scroll position.
 *
 * `will-change: transform` on the media itself (never on `.site-main`)
 * puts it on its own GPU layer. Without it Safari repainted the clipped
 * media on every scroll frame and the scroll was choppy. On the media it
 * is harmless: the only things inside are the absolutely positioned slides.
 *
 * This depends on `.site-main` having NO transform and NO
 * `will-change: transform` at rest, either of which makes it the containing
 * block for fixed descendants. motion.js only puts the nav tilt's transform
 * on while the nav is open, and while it is open motion.js pushes the media
 * down by the scroll offset so it stays where it was.
 *
 * Height is the hero's own height, written by motion.js (`initHeroMedia`)
 * as `--hero-media-height`, so `object-fit: cover` crops the media to the
 * hero frame and centres it there. `100lvh` is the fallback until the
 * script has measured; `lvh` also keeps a mobile toolbar collapse from
 * opening a strip of hero background.
 *
 * The clip carries the hero's own bottom corner radius, since a plain
 * `inset(0)` would square the corners `overflow: hidden` used to round.
 *
 * Every device, desktop and touch (checked on iPhone 15 Pro 2026-10-28),
 * except reduced motion: the OS preference, and `html[data-motion="reduced"]`,
 * which motion.js sets for in-app webviews (Instagram, Facebook, Messenger).
 *
 *   .case-hero__img      case studies, BG and EN (img or video)
 *   .hero-field__video   /uslugi, /en/services
 *   .hero-field__slides  home, /en
 */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) .case-hero,
  html:not([data-motion="reduced"]) .hero-field {
    --hero-media-height: 100lvh;
    clip-path: inset(0 round 0 0 var(--spacing-lg) var(--spacing-lg));
  }

  html:not([data-motion="reduced"]) .case-hero__img,
  html:not([data-motion="reduced"]) .hero-field__video,
  html:not([data-motion="reduced"]) .hero-field__slides {
    position: fixed;
    inset: 0 0 auto;
    width: 100%;
    height: var(--hero-media-height);
    will-change: transform;
  }
}


/* src/styles/page-fade.css */
/* Embrand Studio — page fade overlay.
 *
 * One fixed white sheet, drawn as html::after so it exists at first paint,
 * before <body> has even been parsed. src/scripts/page-fade.js drives it
 * through three states on <html>:
 *
 *   hold    opaque, no animation — from head parse until there is something
 *           to reveal. Without this the fade clock starts when the script
 *           parses, so the sheet is gone before the page has painted and
 *           content appears to land on top of it.
 *   enter   opaque -> transparent   (arriving on a page)
 *   exit    transparent -> opaque   (leaving through an internal link)
 *
 * --page-fade-enter is written inline by the script: 500ms on a cold load,
 * 125ms when the visitor arrived from an internal link. The value below is
 * the fallback if the script never gets to run.
 */

:root {
  --page-fade-enter: 500ms;
  --page-fade-exit: 125ms;
}

html::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 2147483647;
  background: #fff;
  opacity: 0;
  pointer-events: none;
}

/* Holding and closing both swallow pointer events, so nothing behind the
 * sheet can be clicked while it is covering the page. */
html[data-page-fade='hold']::after {
  opacity: 1;
  pointer-events: auto;
}

html[data-page-fade='enter']::after {
  animation: eb-page-fade-enter var(--page-fade-enter) ease forwards;
}

html[data-page-fade='exit']::after {
  animation: eb-page-fade-exit var(--page-fade-exit) ease forwards;
  pointer-events: auto;
}

@keyframes eb-page-fade-enter {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes eb-page-fade-exit {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The script bails out entirely under reduced motion; this is belt and
 * braces so a stale attribute can never leave a sheet on screen. */
@media (prefers-reduced-motion: reduce) {
  html::after {
    display: none;
  }
}


/* src/styles/rich-body.css */
/* Rich-text body content — the markdown-rendered `<instatic-outlet>` regions.
 *
 * Ships as a code asset, not editor CSS, for the same reason `case-figures.css`
 * does: the elements inside a rendered outlet (`<p>`, `<a>`, `<ul>`) are produced
 * by the markdown renderer at publish time, so they are not page-tree nodes and
 * editor class rules that name them would not survive the publish tree-shake
 * (CLAUDE.md §3.1, "Only classes on real nodes survive publish").
 *
 * Consumers:
 *   - `.term-page__body`     — the `branding-termini` post-type template
 *   - `.faq-answer`          — /chesti-vaprosi accordion answers
 *
 * `.glossary-term__body` was a consumer until 2026-09-06, when the
 * /branding-termini index was trimmed from full markdown-rendered bodies to
 * a plain-text `{currentEntry.term}` teaser (`.glossary-term__teaser`) — no
 * outlet, no markdown, no links, so nothing here applies to the index any more.
 *
 * The global reset ships `:where(a) { color: inherit; text-decoration: inherit }`,
 * so an inline link inside body copy is indistinguishable from plain text until
 * it is styled here.
 *
 * 2026-09-06 — tried Lilex italic on the LINKS. Reverted same day: client
 * wanted the italic on the definition prose itself instead, links back to
 * plain Karst SemiBold. Current shape: `.term-page__body p` carries the
 * italic Lilex voice, `.term-page__body a` / `.faq-answer a` explicitly reset
 * back to upright Karst — the reset matters because font-style/font-family
 * are inherited, so without it a link would inherit the paragraph's italic
 * Lilex instead of standing apart from it. The two together read as: flowing
 * italic definition text, with upright bold black anchors breaking through it.
 *
 * 2026-09-06 (later same day) — definition text size, `.term-page__body`
 * only (not `.faq-answer`): first dropped two rungs off the body-copy ladder
 * (body-lg 26 → body 24, the wrapper's own `.text-body` utility → body-sm 17
 * → caption 14), then brought back up one rung to `--text-body-sm-*` (17px /
 * 24px line-height) — the caption tier read too small for running prose.
 * Set explicitly on both the paragraph and the link rule rather than relying
 * on the link inheriting the paragraph's size: the two rules already diverge
 * in font-family (Lilex vs Karst), and Karst/Lilex don't share the same
 * x-height at a given px value, so leaving the link's size to inherit would
 * make "same number" not read as "same size" — stating it on both keeps them
 * visually matched, not just numerically matched.
 *
 * Sizing edits here MUST go through `site_write_code_asset` on this file, not
 * `site_apply_css` — the latter writes to a separate editor-CSS layer and
 * silently produces a second, duplicate ambient rule for the same selector
 * instead of updating this one (caught and reverted 2026-09-06: two stray
 * `.term-page__body p` / `.term-page__body a` rules briefly existed in the
 * editor CSS store alongside these, at a different size).
 *
 * 🔴 2026-09-08 — `site_write_code_asset` REPORTS FAILURE ON A SUCCESSFUL
 * WRITE. Every call to it (and to `site_list_code_assets` /
 * `site_read_code_asset`) returns `Cannot read properties of undefined
 * (reading 'digest')` while the Site editor is connected and healthy — but the
 * write itself lands. A one-line `/* probe *\/` sent to test whether the tool
 * was usable at all silently replaced this entire file, and the 4,318-char
 * original had to be restored from a copy read out of the store. There is no
 * `expectedHash` available to make `site_patch_code_asset` usable either, since
 * the two read tools throw the same error. **Treat the error as advisory, verify
 * off disk, and never send a throwaway payload to a live code asset:**
 *
 *   sqlite3 instatic/.tmp/dev.db \
 *     "select settings_json from site where id='default';" \
 *     | python3 -c "import json,sys; [print(f['path'], len(f['content'])) \
 *         for f in json.load(sys.stdin)['site']['files']]"
 *
 * One property on the link rule is a correction of something inherited, not a
 * free choice:
 *
 *   font-feature-settings: 'ss03' 1
 *     Karst's `.ss03` utility ships fine and is used widely on real nodes, but
 *     a markdown-rendered `<a>` cannot carry a class, so the feature has to be
 *     set as a property here rather than borrowed from the utility.
 */

.term-page__body p {
  font-family: var(--font-mono);
  font-style: italic;
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line-height);
}

.term-page__body a,
.faq-answer a {
  font-family: var(--font-body);
  font-weight: var(--font-weight-semibold);
  font-style: normal;
  font-feature-settings: 'ss03' 1;
  color: var(--black-d-4);
  text-decoration-line: none;
}

.term-page__body a {
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line-height);
}

/* Multi-paragraph bodies: the global reset zeroes every margin, so paragraphs
 * after the first need their own rhythm restored. */
.term-page__body p + p,
.faq-answer p + p {
  margin-top: var(--spacing-base);
}

.term-page__body ul,
.term-page__body ol,
.faq-answer ul,
.faq-answer ol {
  margin-top: var(--spacing-base);
  padding-left: var(--spacing-lg);
}

.term-page__body li,
.faq-answer li {
  margin-top: var(--spacing-xs);
}

/* 2026-09-08 — list rendering, finished. Found via `/privacy`, the first page
 * on the site to put a list inside body prose: measured across all 31
 * published `branding-termini` term pages and all 17 `/chesti-vaprosi`
 * answers, not one body contains a `<li>`, so the four list rules above had
 * shipped since this file was written without ever rendering. Two gaps were
 * invisible for exactly that reason.
 *
 *   list-style-type
 *     The global reset ships `:where(ol, ul, menu) { list-style: none }`, and
 *     nothing here put it back — so items rendered as indented blocks with
 *     4.3px between them, unreadable as a list wherever item boundaries carry
 *     the meaning. Zero specificity, so naming the property at all is enough.
 *     Marker colour left inherited on purpose: a coloured marker is decoration
 *     this design system does not use anywhere else.
 *
 *   the type ramp, `.term-page__body` only
 *     `li` was never given the voice its own `p` carries, so items inherited
 *     the wrapper's `.text-body` utility and came out 24px/36 upright Karst
 *     between 17px/24 italic Lilex paragraphs — louder than the prose they sit
 *     inside, hierarchy inverted. Scoped to `.term-page__body` for the same
 *     reason the size rules above are: `.faq-answer`'s wrapper already carries
 *     `.text-body-sm-emphasis`, which IS Lilex italic at `--text-body-sm-*`,
 *     so its items inherit the right voice and restating it here would be
 *     duplicating a value that is already correct.
 *
 * `/privacy` does not consume any of this — it was built on its own
 * `.legal-doc__*` classes by client direction (match the Glossary's
 * typography, don't share its classes; build-log 2026-09-08). This fix is so
 * the next glossary definition or FAQ answer that needs a list gets one that
 * reads correctly, rather than reproducing the bug `/privacy` surfaced.
 */
.term-page__body li {
  font-family: var(--font-mono);
  font-style: italic;
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line-height);
}

.term-page__body ul,
.faq-answer ul {
  list-style-type: disc;
}

.term-page__body ol,
.faq-answer ol {
  list-style-type: decimal;
}

/* 2026-09-23 — subheadings and figures, `.term-page__body` only. Twelve terms
 * grew into 300+ word explainers with `##` sections and one project image each
 * (DOCS/plans/2026-09-23-glossary-extension-proposal.md). Before this, no term
 * body carried an `h2` or an `img`, so both rendered unstyled: the `h2` took
 * the wrapper's inherited 36px bold with zero margin, larger than the page's
 * own H1 (`text-h1`, ~32px here) and glued to the paragraph above it.
 *
 *   h2  — the `text-h3` tokens (28/36 display), one rung under the page H1, with
 *         `--spacing-xl` above to open each section and `--spacing-sm` below.
 *   img — full column width, block, `--radius-md` like the case figures. Later
 *         the same day the image moved to the top of each body (layout:
 *         breadcrumb, H1, example image, body), so it carries no top margin;
 *         the paragraph after it gets its space from `p + p`.
 *   caption — a paragraph holding only an emphasis (`*…*` in markdown) under a
 *         figure reads as a caption: caption size, secondary text colour. Used
 *         for the rights note under the BOOM! stylescape. Colour moved to
 *         `--charcoal-400` the same day at the owner's request ("much paler
 *         than main text"): 3.4:1 on white, below AA for 14px text, accepted
 *         for a rights note — same trade-off as the parked plan §5.2.
 */
.term-page__body h2 {
  margin-top: var(--spacing-xl);
  margin-bottom: var(--spacing-sm);
  font-family: var(--text-h3-family);
  font-weight: var(--text-h3-weight);
  font-size: var(--text-h3-size);
  line-height: var(--text-h3-line-height);
  letter-spacing: var(--text-h3-tracking);
}

.term-page__body img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}

.term-page__body p > em:only-child {
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-line-height);
  color: var(--charcoal-400);
}
