@import url("/assets/ui-chrome.css");
@import url("/assets/brand-lockup.css");
/* ============================================================================================
   RAVENOR — shared page shell
   --------------------------------------------------------------------------------------------
   Every page that is NOT the homepage loads this file. The homepage keeps its own inline CSS:
   it carries the frame-scrub hero and a large amount of one-off machinery, and lifting that into
   a shared file would have meant rewriting a page that is already accepted and in production.
   So the rule here is narrow and deliberate — this file owns the FURNITURE (header, footer,
   language control, quote drawer, page rhythm) and copies the homepage's tokens EXACTLY, so a
   visitor moving from / to /projects sees the same header land in the same place, in the same
   colour, at the same height.

   Tokens below are byte-identical to the homepage's :root. If one is changed there it must be
   changed here; that duplication is the price of not rewriting the hero, and it is written down
   rather than left to be discovered.

   Design register: a fabrication drawing. Hairline rules, tabular index numbers, monospace
   micro-labels, generous negative space, and sage used as a PRECISION ACCENT — never as a fill.
   ============================================================================================ */

:root{
  --base:#0B0D0C;--legal:#0C0F0E;--inset:#0E1110;--strip:#0E1211;--surface:#121517;--modal:#14181A;
  --tagbg:#15191B;--raised:#16191B;
  --div:#1C2220;--border:#23282A;--border-in:#2A2F31;--border-ctl:#2C3331;--chip-bd:#2C3A30;--ghost-bd:#5A7A64;
  /* WCAG 1.4.11 applies to the boundary of a CONTROL — the line that tells a visitor
     "this is a field you can type in" or "this is a choice you can press". It does not
     apply to decorative framing or to dividers, so --div, --border and the card hairlines
     above keep the restraint the owner approved and only the interactive edges are lifted.
     Measured against the real surfaces: --border-field 3.26:1 on the input inset and
     3.38:1 on the page base; --chip-bd-on 3.22:1 on the chip fill. */
  --border-field:#5C6862;--chip-bd-on:#647063;
  --text:#ECEFF1;--text-hi:#F3F6F5;--text-rev:#CDD2D1;--chip-tx:#C5D0C6;--muted:#97A09C;
  --card-tx:#8B9290;--faint:#7D8783;--faint-2:#7A8480;
  --green:#ABC9B0;
  --sage:var(--green,#ABC9B0);--sage-br:#C2DCC6;--on-sage:#15241B;--sage-chip:#15201A;--steel:#C4CBCE;
  --danger:#cf6a6a;
  --font-display:'Orbitron','Noto Sans Armenian','Inter',sans-serif;
  /* Consolas and Cascadia Mono are not decoration: the stack used to be macOS-only
     (ui-monospace / SF Mono / Menlo), so on Windows every mono label in the product fell through
     to whatever the browser's default fixed font happens to be, with wider advances than anything
     the layouts were measured against. Noto Sans Armenian is the tail because Consolas has no
     Armenian coverage, and without it hy mono text was filled per-glyph by an unmanaged system
     face. Keep this identical to the copy in site/index.html or the two shells drift again. */
  --font-mono:ui-monospace,'SF Mono',Menlo,Consolas,'Cascadia Mono','Noto Sans Armenian',monospace;
  --font-body:'Inter','Noto Sans Armenian',system-ui,sans-serif;
  --font-brand:'Orbitron',sans-serif;          /* RAVENOR never follows the interface language */

  /* ------------------------------------------------------------------ MOTION SYSTEM
     Four durations and three curves, and every animated property on the site picks from them.
     Before this the shell held .18s, .2s, .22s, .24s, .25s, .28s, .3s, .34s, .35s, .42s, .45s,
     .5s, .6s and .7s with three different curves — differences nobody chose and nobody can see,
     which is what makes a site feel assembled rather than designed.

     The register is a workshop, not an app: things have mass, they accelerate under load and
     they stop without bouncing. There is no spring anywhere.

       --ease       the settle. Most things.
       --ease-mech  weighted and mechanical — the approved Quote drawer curve. For objects with
                    real mass: panels, sheets, anything large arriving or leaving.
       --ease-line  symmetric, no overshoot. For a line being DRAWN — rules, underlines, the back
                    control's hairline. A drawn line that eases out has been flicked, not scribed.

       --t-fast  a state flips: colour, opacity, a hover tint.
       --t-ctl   a control acknowledges you.
       --t-move  something travels a real distance.
       --t-mass  something with mass is repositioned.
     ------------------------------------------------------------------------------------------ */
  --ease:cubic-bezier(0.22,1,0.36,1);
  --ease-mech:cubic-bezier(.16,.84,.24,1);
  --ease-line:cubic-bezier(.65,0,.35,1);
  --t-fast:.18s;--t-ctl:.28s;--t-move:.42s;--t-mass:.52s;

  --tr-brand:.34em;--tr-eyebrow:.42em;--tr-label:.26em;--tr-nav:.2em;--tr-display:.12em;
  --lh-display:1.12;
  --sz-eyebrow:1;--sz-label:1;--w-eyebrow:500;
  --hh:70px;                                    /* header height; shell.js keeps it in sync */
  --rail:1200px;                                /* the one content rail, shared with the homepage */
}

/* Locale typography, carried over rule for rule from the homepage. Orbitron is Latin-only, so
   Russian and Armenian get a face that actually covers the script — and, just as importantly,
   their own tracking and size, because Latin caps survive wide tracking and these scripts do not. */
html[lang="ru"]{--font-display:'Inter',system-ui,sans-serif;
  --tr-eyebrow:.14em;--tr-label:.11em;--tr-nav:.08em;--tr-display:.04em;--lh-display:1.18;
  --sz-eyebrow:1.12;--sz-label:1.14;--w-eyebrow:600}
html[lang="hy"]{--font-display:'Noto Sans Armenian','Inter',system-ui,sans-serif;
  --tr-eyebrow:.05em;--tr-label:.04em;--tr-nav:.03em;--tr-display:.02em;--lh-display:1.28;
  --sz-eyebrow:1.22;--sz-label:1.20;--w-eyebrow:600}

*{margin:0;padding:0;box-sizing:border-box}
/* The `hidden` attribute is the accessible way to say "this is not here yet" - it hides the node
   from the accessibility tree as well as from the screen. But the UA rule behind it is a bare
   `[hidden]{display:none}`, which ANY class carrying a display value outranks: `.pj-filter
   {display:flex}` on a hidden element renders it. Restoring it here means a page can use the
   attribute and be believed. */
[hidden]{display:none!important}
/* The metal backdrop is the brand's real material. Identical declaration to the homepage so the
   two pages share one continuous surface rather than two similar ones. */
html{background:
  radial-gradient(82% 50% at 50% -10%,rgba(171,201,176,.10),transparent 60%),
  linear-gradient(180deg,rgba(10,12,11,.74),rgba(10,12,11,.90)),
  url('/metal_bg.webp') center top / cover no-repeat,
  var(--base);background-attachment:fixed;-webkit-text-size-adjust:100%}
body{background:transparent;color:var(--text);font-family:var(--font-body);font-size:16px;
  line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:clip}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
button,input,textarea,select{font-family:inherit;font-size:inherit;color:inherit}
button{cursor:pointer;background:none;border:0}
::selection{background:rgba(171,201,176,.26)}
h1,h2,h3,h4{font-family:var(--font-display);font-weight:500;letter-spacing:var(--tr-display);
  line-height:var(--lh-display);overflow-wrap:break-word}
.brand,.rv-word{font-family:var(--font-brand);text-transform:none;font-variant-ligatures:none;
  font-feature-settings:normal;line-height:1.1}

/* ================================================================================ LOCKUP SYSTEM
   ONE set of proportions for every place the mark stands beside the word RAVENOR - header, footer,
   Assistant, Admin, mobile - because the owner's complaint was never about a single surface. It
   was that the surfaces disagreed with each other, and a brand that disagrees with itself reads as
   an accident rather than an identity.

   The proportions are MEASURED FROM THE ARTWORK, not invented here. In brand/ravenor_main_logo.png
   - the supplied master lockup - the mark's ink box is 242x333 beside a wordmark whose cap height
   is 73px, the gap between them is 19px, and the wordmark's optical centre sits 36px BELOW the
   mark's centre. Expressed against the cap height, which is the only unit that survives a change
   of type size:

       mark height : cap height  = 4.56
       gap         : cap height  = 0.26
       word drop   : mark height = 0.108
       mark aspect                = 552/763  (the icon file's own pixels)

   The site shipped a mark-to-cap ratio of 0.83em/0.72em = 1.15 on the header and footer and 2.26
   on the Assistant. At 1.15 the mark is barely taller than the letters next to it, so it stops
   reading as a mark at all and becomes a small green chip stuck to the front of a word - which is
   what the owner kept seeing and calling wrong.

   4.56 is a POSTER proportion: it assumes the wordmark is small and the mark carries the whole
   composition. Reproduced literally in a 44px navigation bar it would demand a mark taller than
   the bar, or a wordmark too small to read. So the system takes the master's RELATIONSHIPS - the
   gap and the optical drop exactly, the mark dominant rather than incidental - and sets the one
   free variable, the ratio, by rendering 1.15 / 2.0 / 2.6 / 3.2 against the real footer and
   comparing them. Below 2.0 the mark still reads as an afterthought; at 3.2 it overpowers a single
   line of type and starts colliding with the tagline beneath. 2.6 is the strongest composition
   that keeps the wordmark an equal partner, and at the header's 13px cap it puts the mark at 34px
   - inside the 44px the navigation already occupies, so --hh does not move and the hero handoff
   that depends on it is untouched.

   Everything is expressed in em against the LOCKUP's own font-size, so one token governs all five
   surfaces and a surface only has to declare how big its wordmark is. */
/* Geometry tokens now live in brand-lockup.css, shared with Admin. */
/* `aspect-ratio` rather than trusting the width/height attributes: the footer shipped 17x24, which
   is 0.708 against the artwork's 0.7235, so the mark was being squeezed by about two percent. */
.rv-lockup-mark{height:var(--brand-mark-h,1.872em);width:auto;aspect-ratio:var(--brand-mark-aspect,552 / 763);
  flex:none;transform:none;object-fit:contain}
.rv-lockup-word{position:static}

/* Accessibility: a keyboard user must always be able to see where they are. `:focus-visible`
   rather than `:focus` so a mouse click does not paint a ring the pointer user never asked for. */
:focus-visible{outline:2px solid var(--sage);outline-offset:3px;border-radius:4px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* Skip link — the first tab stop on every page, so a screen-reader or keyboard visitor is not
   forced through the whole header on each navigation. */
.skip{position:absolute;left:12px;top:-60px;z-index:200;background:var(--sage);color:var(--on-sage);
  padding:11px 18px;border-radius:var(--r-control,2px);font-size:13px;font-weight:600;transition:top var(--t-ctl) var(--ease)}
.skip:focus{top:12px}

/* ---------------------------------------------------------------- header */
/* The homepage header is not just "translucent with a blur" — it carries a sage edge-light along
   its bottom, a soft shadow under it, a richer scrolled state, and two responsive tiers that were
   tuned against Armenian and Russian string widths. Reproducing the base rule alone got the
   geometry right and the CHARACTER wrong, which is exactly how a subpage starts feeling like a
   different site. Everything below is the homepage's own treatment, rule for rule. */
/* THE HEADER AS ITS OWN MATERIAL — the owner-approved treatment, copied from the homepage's
   authoritative block, not from the simpler flat-tint rule that sits earlier in that file. The bar
   has to read as premium dark glass whether a photograph is behind it or nothing at all, so the
   depth is built INSIDE it: a graphite body with a vertical fall, one wide low internal
   illumination suggesting light from above, a fine top sheen on the inner edge, and the sage
   edge-light below. backdrop-filter still runs and enriches it further, but nothing depends on it.
   Copying only the flat rgba(10,13,11,.5) is what made /projects feel like a cheaper page. */
header.rv-hdr{position:fixed;top:0;left:0;width:100%;z-index:60;display:flex;align-items:center;
  justify-content:space-between;gap:16px;padding:19px clamp(20px,4vw,56px);
  background:
    radial-gradient(130% 320% at 50% -150%,rgba(216,232,219,.115),rgba(196,214,200,.040) 40%,transparent 70%),
    radial-gradient(60% 200% at 8% -60%,rgba(171,201,176,.052),transparent 68%),
    linear-gradient(180deg,rgba(20,25,22,.78) 0%,rgba(14,18,16,.85) 56%,rgba(10,13,11,.90) 100%);
  box-shadow:inset 0 1px 0 rgba(236,246,238,.075),0 12px 36px -26px rgba(0,0,0,.92);
  backdrop-filter:blur(20px) saturate(1.22);-webkit-backdrop-filter:blur(20px) saturate(1.22);
  border-bottom:1px solid transparent;
  transition:background var(--t-mass) var(--ease),border-color var(--t-mass) var(--ease)}
/* the sage edge-light under the glass — the signature that says this bar is Ravenor's */
header.rv-hdr::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  pointer-events:none;
  background:linear-gradient(90deg,transparent,rgba(171,201,176,.26) 20%,rgba(171,201,176,.36) 50%,
    rgba(171,201,176,.26) 80%,transparent);opacity:1}
header.rv-hdr::before{content:"";position:absolute;left:0;right:0;bottom:-14px;height:14px;
  pointer-events:none;background:linear-gradient(180deg,rgba(6,8,7,.42),transparent);opacity:.9}
header.rv-hdr.scrolled{
  background:
    radial-gradient(130% 320% at 50% -150%,rgba(216,232,219,.075),transparent 66%),
    linear-gradient(180deg,rgba(15,19,17,.86) 0%,rgba(10,13,11,.91) 100%);
  backdrop-filter:blur(24px) saturate(1.25);-webkit-backdrop-filter:blur(24px) saturate(1.25)}

/* Fixed-header clearance for native anchor jumps, same as the homepage. */
html{scroll-padding-top:84px}
.rv-hdr .brand{display:flex;gap:var(--brand-mark-gap,.187em);font-weight:700;
  font-size:clamp(16px,2vw,22px);letter-spacing:var(--tr-brand);color:var(--text-hi)}
.rv-hdr .brand img{height:var(--brand-mark-h,1.872em);width:auto;aspect-ratio:var(--brand-mark-aspect,552 / 763);
  flex:none;object-fit:contain;filter:drop-shadow(0 0 14px rgba(171,201,176,.35))}
.rv-hdr .brand .rv-brand-word{position:static}
/* --sz-label / --w-eyebrow and a pinned line-height, matching site/index.html. Bare 11px/400 left
   the header as the one place in the product not compensated per script, and line-height:normal
   let the whole bar - and --hh with it - change height between EN, RU and HY. */
.rv-nav{display:flex;align-items:center;gap:clamp(12px,1.9vw,28px);font-family:var(--font-display);
  font-size:calc(11px * var(--sz-label));font-weight:var(--w-eyebrow);line-height:1;
  letter-spacing:var(--tr-nav);text-transform:uppercase;color:var(--text-rev)}
.rv-nav>a:not(.nav-quote){position:relative;opacity:.82;padding:4px 0;
  transition:opacity var(--t-ctl),color var(--t-ctl)}
.rv-nav>a:not(.nav-quote)::after{content:'';position:absolute;left:0;right:100%;bottom:-2px;
  height:1px;background:var(--sage);transition:right var(--t-move) var(--ease-line)}
.rv-nav>a:not(.nav-quote):hover{opacity:1;color:var(--text-hi)}
.rv-nav>a:not(.nav-quote):hover::after{right:0}
/* ------------------------------------------------------------------ THE DIMENSION MARK
   Ravenor measures before it cuts. So the directional affordance on this site is not an
   arrow — it is a DIMENSION off a shop drawing: a rule that runs between two points with a
   single terminator at the end of it.

   At rest the measurement is closed: a short rule and a terminator sitting near its origin,
   quiet enough to sit inside body copy without shouting. On hover the rule is DRAWN OUT to
   full length and the terminator travels to the new end — which is what taking a measurement
   actually looks like, and reads as the link extending toward its destination.

   Two things this replaces, which were the whole reason the site felt assembled rather than
   designed: the homepage's old mark RETRACTED on hover (scaleX(.72)) — a measurement being
   taken back — and every other page drew a generic 14px →  from shell.js's sprite. One
   vocabulary now, and it belongs to this client rather than to any dark template.
   --------------------------------------------------------------------------------------- */
.link-mark{display:inline-block;position:relative;flex:none;width:26px;height:10px;
  margin-left:10px;vertical-align:middle;color:inherit}
/* The measurement rule. The two halves have to READ AS ONE OBJECT at rest, and the geometry
   for that is not obvious: a 6px square carrying border-top + border-right, rotated 45deg,
   puts its two free stroke ends directly ABOVE and BELOW its own centre — so the back of the
   terminator is at its centre x, not at its left edge. A rule stopping at the square's left
   edge therefore leaves a visible 4px hole, which is exactly what the first attempt rendered.

   Terminator centre at rest = 26 - 1(right) - 3(half) - 7(translate) = 15px, so the rule runs
   to 15/26 = .58. Measured off a 10x magnified render of the shipped page, not estimated. */
.link-mark::before{content:'';position:absolute;left:0;top:50%;width:100%;height:1px;
  background:currentColor;opacity:1;transform:translateY(-50%) scaleX(.58);
  transform-origin:left center;
  transition:transform var(--t-move) var(--ease-line),opacity var(--t-ctl) var(--ease)}
/* the terminator */
.link-mark::after{content:'';position:absolute;right:1px;top:50%;width:6px;height:6px;
  border-top:1px solid currentColor;border-right:1px solid currentColor;
  transform:translate(-7px,-50%) rotate(45deg);
  transition:transform var(--t-move) var(--ease-line)}
a:hover .link-mark::before,button:hover .link-mark::before,
a:focus-visible .link-mark::before,button:focus-visible .link-mark::before,
.rv-card:hover .link-mark::before,.sv-card:hover .link-mark::before,
.pj-card:hover .link-mark::before,.home-project:hover .link-mark::before,
.service-line:hover .link-mark::before{transform:translateY(-50%) scaleX(1);opacity:1}
a:hover .link-mark::after,button:hover .link-mark::after,
a:focus-visible .link-mark::after,button:focus-visible .link-mark::after,
.rv-card:hover .link-mark::after,.sv-card:hover .link-mark::after,
.pj-card:hover .link-mark::after,.home-project:hover .link-mark::after,
.service-line:hover .link-mark::after{transform:translate(0,-50%) rotate(45deg)}
/* A mark that is being read back to the visitor (a back control, a previous slide) measures
   the other way. Same object, mirrored — not a second icon. */
.link-mark.is-back{transform:scaleX(-1)}
@media (prefers-reduced-motion:reduce){
  .link-mark::before,.link-mark::after{transition:none}
}

/* The text link the mark lives inside. Defined here rather than in home.css so the shell
   pages can use the same control instead of inventing `.sv-go` a second time. */
/* RAVENOR ACCENT — the owner marks a word in Admin with [[double brackets]] and it carries the
   brand sage. See server/rich_text.py for the syntax and why it is that one.

   COLOUR ONLY, AND ON PURPOSE. No weight change, no underline, no background: the word is
   already in the reader's line, and the point is that it glows rather than that it shouts. It
   inherits everything else from its parent, so one mark behaves identically in a 64px headline
   and in body copy, and it never reflows the line it sits in — a weight change would.

   It is emphasis, not information. Nothing is communicated by the colour alone that the sentence
   does not already say, so there is no meaning for a reader who cannot see the hue to miss. */
.rv-accent{color:var(--sage);font:inherit;letter-spacing:inherit}
@media (forced-colors:active){.rv-accent{color:Highlight}}

.text-link{display:inline-flex;align-items:center;font-size:13px;color:var(--text);
  min-height:44px;transition:color var(--t-ctl) var(--ease)}
.text-link svg{width:var(--ico-md,18px);height:var(--ico-md,18px)}
.text-link:hover,.text-link:focus-visible{color:var(--sage)}
/* aria-current is the accessible truth of "you are here"; styling from it means the highlight can
   never disagree with what a screen reader announces. */
.rv-nav>a[aria-current="page"]{opacity:1;color:var(--text-hi)}
.rv-nav>a[aria-current="page"]::after{right:0}
.nav-quote{border:1px solid var(--ghost-bd);color:var(--sage)!important;opacity:1!important;
  padding:9px 17px;border-radius:var(--r-control,2px);font-weight:var(--w-eyebrow);line-height:1;
  letter-spacing:var(--tr-nav);transition:var(--t-ctl) var(--ease)}
.nav-quote:hover{background:var(--sage);color:var(--on-sage)!important;border-color:var(--sage);
  box-shadow:0 8px 26px -12px rgba(171,201,176,.5)}

/* language control — the homepage's globe + dropdown, not a second pattern */
.lang-wrap{position:relative}
.globe{display:flex;align-items:center;gap:6px;color:#cfd6d0;font-family:var(--font-display);
  font-size:calc(11px * var(--sz-label));font-weight:var(--w-eyebrow);line-height:1;
  letter-spacing:var(--tr-nav);padding:6px 4px}
.globe svg{width:var(--ico-sm,16px);height:var(--ico-sm,16px)}
.globe .cv{width:var(--ico-xs,12px);height:var(--ico-xs,12px);transition:transform var(--t-ctl) var(--ease)}
.globe[aria-expanded="true"] .cv{transform:rotate(180deg)}
.lang-menu{position:absolute;top:calc(100% + 10px);right:0;min-width:150px;background:var(--modal);
  border:1px solid var(--border-in);border-radius:10px;padding:6px;
  box-shadow:0 18px 44px rgba(0,0,0,.55);opacity:0;transform:translateY(-6px);pointer-events:none;
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease);z-index:70}
.lang-menu.open{opacity:1;transform:none;pointer-events:auto}
.lang-menu a{display:block;padding:9px 12px;border-radius:var(--r-control,2px);font-family:var(--font-body);
  font-size:13.5px;letter-spacing:0;text-transform:none;color:var(--text-rev);cursor:pointer}
.lang-menu a:hover{background:rgba(171,201,176,.10);color:var(--text-hi)}
.lang-menu a[aria-current="true"]{color:var(--sage)}

.menu-btn{display:none;color:var(--text-rev);padding:6px;
  transition:background var(--t-ctl,.15s) ease,color var(--t-ctl,.15s) ease}
/* It answered nothing — not the pointer, not a press, and not its own open state, although it IS
   the close control. It appears below 1140px, which includes a narrow laptop window driven by a
   mouse, so hover is real here and not only a phone concern. */
.menu-btn:hover{color:var(--sage);background:rgba(171,201,176,.08)}
.menu-btn:active{background:rgba(171,201,176,.16)}
/* OPEN IS VISIBLE, without a pointer anywhere near it. A close control that looks the same open
   and shut gives nobody a way to tell what pressing it will do. */
.menu-btn[aria-expanded="true"]{color:var(--sage);background:rgba(171,201,176,.12)}
.menu-btn:focus-visible{outline:2px solid var(--sage);outline-offset:2px}
/* On touch `:hover` sticks after the tap, so the burger would stay tinted after the panel closed
   and report "open" about a closed menu. Touch keeps the expanded state and nothing else. */
@media(hover:none){.menu-btn:hover{color:var(--text-rev);background:transparent}
  .menu-btn[aria-expanded="true"]:hover{color:var(--sage);background:rgba(171,201,176,.12)}}
@media(prefers-reduced-motion:reduce){.menu-btn{transition:none}}
.menu-btn svg{width:24px;height:24px}
.menu-btn svg{width:24px;height:24px}
/* The homepage's mobile menu is a panel that drops from under the bar, not a full-screen takeover.
   Two different menu gestures on one site is exactly the kind of drift the shared shell exists to
   remove, so this is the homepage's pattern, rule for rule — including `top:var(--hh)`, which is
   why the header keeps that variable in sync. */
.rv-mm{display:none;position:fixed;top:var(--hh,62px);left:0;right:0;z-index:59;
  background:rgba(11,14,12,.98);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  border-bottom:1px solid rgba(40,48,40,.9);flex-direction:column;padding:10px 24px 22px;
  max-height:calc(100vh - var(--hh,62px));overflow-y:auto}
.rv-mm.open{display:flex}
/* The menu items were 20px type in full-width rectangles divided by hairlines - three separate
   reasons it read as heavy. They are now softly contained instead of ruled apart: the hairline
   divider goes (the container IS the separation), the type drops to the shell's own nav size, and
   each item gets a quiet surface with a 10px radius. Still 44px+ under a thumb, just not a stack
   of boxes. */
.rv-mm a{color:#dfe6e0;font-family:var(--font-display);font-size:14px;letter-spacing:var(--tr-nav);
  text-transform:uppercase;padding:15px 4px;border-bottom:1px solid rgba(40,48,40,.45)}
/* YOU ARE HERE, in the panel.
   `.rv-nav>a[aria-current]` has carried the desktop treatment since the shell was written; the
   panel carried NONE - measured, not assumed: the attribute was on the row and no rule matched
   it, so on a phone the open menu looked identical on every page. That is the owner's "the
   current page is not obvious" and "the user should always understand where they are".

   Stated once here rather than inside the 820px block, because the panel is on screen from
   1139px down and an active state that only exists below 820 is the same defect in a narrower
   window. An inset shadow, not a border or a padding change: the row's box must not move when
   it becomes current, or the menu reflows as you navigate.

   The carrier is a 3px rule down the leading edge plus a lift in text and surface - a position
   and a shape, not only a hue, so it survives a monochrome or low-contrast view. A trailing dot
   was tried and removed: `product.css` makes these rows `display:flex` on the inner pages and
   not on the home page, so the same pseudo-element sat in two different places. */
.rv-mm a[aria-current="page"]{color:var(--text-hi);
  box-shadow:inset 3px 0 0 0 var(--sage);
  background:rgba(171,201,176,.10)}
@media(max-width:820px){
  .rv-mm{border-radius:0 0 16px 16px;padding:14px 20px 20px}
  /* `body` prefix on purpose: product.css sets these items to 20px and loads after this file,
     so an equal-specificity rule here would lose. */
  body .rv-mm a{font-size:15px;padding:13px 14px;border-bottom:0;border-radius:10px;
    background:rgba(171,201,176,.045);margin-bottom:7px;min-height:46px}
  .rv-mm a:active{background:rgba(171,201,176,.10)}
  /* body-prefixed for the same reason as the items: product.css styles `.rv-mm>.nav-quote`
     separately at 20px, so without it the menu CTA stayed a third larger than everything
     above it - visible in Armenian, where the label is longest. */
  body .rv-mm .nav-quote{border-radius:var(--r-control,6px);margin-top:12px;font-size:15px}
  .rv-mm .mm-langs{gap:8px;margin-top:14px}
  .rv-mm .mm-langs button{border-radius:8px}
  .menu-btn{border-radius:8px}
}
/* `.rv-mm .nav-quote`, not `.rv-mm a.nav-quote`: the control is a <button> now, so the anchor
   rule above no longer supplies its font, size or tracking. Stated here so the styling is not
   coupled to the element type a second time. Same fix the homepage already carries. */
.rv-mm .nav-quote{color:var(--sage);border:1px solid var(--sage);border-radius:var(--r-control,2px);
  text-align:center;margin-top:16px;padding:14px;border-bottom-color:var(--sage);
  font-family:var(--font-display);font-size:14px;letter-spacing:var(--tr-nav);
  text-transform:uppercase;min-height:44px}
.rv-mm .mm-langs{display:flex;gap:10px;margin-top:18px}
.rv-mm .mm-langs button{flex:1;padding:12px;background:transparent;
  border:1px solid rgba(40,48,40,.9);color:#cfd6d0;border-radius:var(--r-control,2px);
  font-family:var(--font-display);font-size:12px;letter-spacing:var(--tr-nav)}
.rv-mm .mm-langs button[aria-pressed="true"]{background:var(--sage);color:#0a0f0a;border-color:var(--sage)}

/* Compact desktop, 1140–1440: the homepage tightens the nav rhythm here so the full menu still
   fits at 1140px in Armenian, which is the widest of the three scripts. Without this the shell
   measured a 27px gap where the homepage measured 18px — the same links, visibly further apart. */
@media(min-width:1140px) and (max-width:1440px){
  .rv-nav{gap:clamp(12px,1.35vw,18px)}
  .nav-quote{padding:8px 14px}
}
/* The menu switch is at 1139px, not 1000px, so the shell and the homepage change shape at exactly
   the same width — otherwise a visitor resizing between 1000 and 1139 sees one page with a burger
   and the other with a full nav. */
@media(max-width:1139px){
  .rv-nav{display:none}
  .menu-btn{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0}
  /* 44px is the accessible tap target; the padding comes off the bar instead, which is what took
     the homepage's phone header from 81px to 57px. */
  header.rv-hdr{padding:6px clamp(14px,4vw,22px)}
  .rv-hdr .brand{font-size:clamp(15px,2.2vw,20px);gap:var(--brand-mark-gap)}
  /* mark height comes from --brand-mark-h at every width; see the LOCKUP SYSTEM block */
}
@media(max-width:360px){ .rv-hdr .brand{font-size:14px;gap:var(--brand-mark-gap,.187em)} }
/* TOUCH TARGETS on the two standalone header controls.
   Measured across 147 route/width/language combinations: the RAVENOR home link resolved to a
   20-24px tap target and the language globe to 27px. Both clear WCAG 2.5.8 AA (24px) only
   marginally, and both are things people reach for on a phone while holding it one-handed.

   Scoped to `pointer:coarse` deliberately. Applying a 44px minimum unconditionally would grow the
   DESKTOP header from its measured 70px to 82px, and that header geometry is owner-approved and
   shared byte-for-byte with the homepage. On a touch device the bar is already 56px tall, so 44px
   fits inside it and nothing moves. */
@media(pointer:coarse){
  .rv-hdr .brand,.globe{min-height:44px}
  .globe{padding:6px 8px}
}

/* ============================================================================================
   BACKGROUND VOCABULARY
   --------------------------------------------------------------------------------------------
   Five surfaces, chosen once, so no page invents its own. Every one of them is graphite first —
   the sage and the light are accents that arrive at a decision point, never a wash. All five are
   built from gradients on a pseudo-element, so there is no image to download, nothing to decode
   on a phone, and `prefers-reduced-motion` can switch the only animated one off completely.

   A  material    near-solid graphite. The default; the page's own content carries it.
   B  steel       graphite + a fine directional grain, like a brushed plate under low light.
   C  atmosphere  graphite + one controlled sage/white light entering from above.
   D  technical   a drawing surface: dot lattice, masked so it fades before it reaches the text.
   E  field       the contained interactive surface, used at exactly one conversion moment.

   MOBILE IS NOT A SMALLER DESKTOP. Each surface re-tunes its own scale below 760px rather than
   letting a desktop gradient get squeezed: the light source moves up and tightens, the grain
   coarsens so it survives a 2x screen, and the lattice grows so it does not turn into moiré.

   THE SURFACE IS FULL-BLEED, THE CONTENT IS NOT. `.rv-page-head` lives inside the 1200px rail,
   so a surface painted at `inset:0` was a 1200px-wide rectangle of light sitting on a 1440px
   page — with two visible vertical edges down the screen and a hard horizontal edge where it
   stopped. Light does not have corners. The surface now escapes the rail with a symmetric
   negative inline inset and fades out at its foot instead of ending, so what the visitor sees is
   an illuminated REGION of the page rather than a panel laid on top of it.
   ============================================================================================ */
.rv-bg{position:relative;isolation:isolate}
.rv-bg::before{content:'';position:absolute;z-index:-1;pointer-events:none;
  /* escape the content rail on both sides — 50vw each way is always enough and the overflow is
     clipped by `body{overflow-x:clip}`, so this can never create a scrollbar */
  top:0;bottom:0;left:-50vw;right:-50vw;
  /* and stop by fading, not by ending */
  -webkit-mask-image:linear-gradient(180deg,#000 62%,transparent);
  mask-image:linear-gradient(180deg,#000 62%,transparent)}

/* A — material. The default graphite base. Used where a PHOTOGRAPH is the subject and the
   background's only job is to stop competing with it: a shallow fall from the header, one very
   low sage bias so it is still Ravenor's black and not a neutral one, and nothing else. */
.bg-material::before{
  background:
    linear-gradient(180deg,rgba(24,29,26,.60),rgba(12,15,13,.30) 46%,transparent 78%),
    radial-gradient(120% 44% at 50% -12%,rgba(171,201,176,.055),transparent 68%)}

/* B — brushed steel. Two hairline gradients at opposing angles read as a machined surface; at
   1px they would shimmer, so the stripe is 3px and the contrast is a few percent. */
.bg-steel::before{
  background:
    repeating-linear-gradient(102deg,rgba(255,255,255,.014) 0 1px,transparent 1px 3px),
    repeating-linear-gradient(-78deg,rgba(0,0,0,.10) 0 1px,transparent 1px 4px),
    linear-gradient(180deg,rgba(18,22,20,.55),transparent 62%)}

/* C — atmosphere. One light, high and wide, plus a floor shadow so the section sits on something. */
.bg-atmos::before{
  background:
    radial-gradient(64% 62% at 50% -14%,rgba(171,201,176,.13),rgba(171,201,176,.035) 42%,transparent 72%),
    radial-gradient(34% 40% at 72% 6%,rgba(226,238,229,.05),transparent 64%),
    linear-gradient(180deg,transparent 55%,rgba(6,8,7,.42))}

/* D — technical drawing surface. The lattice is masked to fade out before it reaches the text, so
   it reads as a drafting sheet rather than as a pattern behind a paragraph. */
.bg-grid::before{
  background-image:
    radial-gradient(rgba(171,201,176,.15) 1px,transparent 1px),
    linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px),
    linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px);
  background-size:26px 26px,26px 26px,26px 26px;
  background-position:center;
  -webkit-mask-image:radial-gradient(46% 62% at 50% 34%,#000 18%,transparent 78%);
  mask-image:radial-gradient(46% 62% at 50% 34%,#000 18%,transparent 78%)}

/* ---- MOBILE ART DIRECTION -----------------------------------------------------------------
   Not a scale-down. On a phone the head is roughly 300px of a 780px screen and the eye lands at
   the top of it, so every surface moves its light UP and narrows it: on a 1440 page the light is
   a wide wash behind a title, on a 390 page the same wash across a narrower box turns the title
   grey. The grain and the lattice go the other way and get COARSER, because a 3px stripe and a
   26px lattice are sub-pixel structures at 3x and read as noise or as moiré.
   -------------------------------------------------------------------------------------------- */
@media(max-width:760px){
  /* the light comes from higher and narrower so it lands above the title, not across it */
  .bg-atmos::before{
    background:
      radial-gradient(88% 40% at 50% -6%,rgba(171,201,176,.17),rgba(171,201,176,.045) 46%,transparent 76%),
      linear-gradient(180deg,transparent 58%,rgba(6,8,7,.5))}
  /* a 3px stripe disappears on a 2x phone screen; coarsen it so the material is still readable */
  .bg-steel::before{
    background:
      repeating-linear-gradient(102deg,rgba(255,255,255,.020) 0 1px,transparent 1px 5px),
      linear-gradient(180deg,rgba(18,22,20,.5),transparent 66%)}
  /* a 26px lattice turns into moiré on a phone; open it up and pull the mask in */
  .bg-grid::before{background-size:34px 34px,34px 34px,34px 34px;
    -webkit-mask-image:radial-gradient(64% 44% at 50% 22%,#000 12%,transparent 72%);
    mask-image:radial-gradient(64% 44% at 50% 22%,#000 12%,transparent 72%)}
  /* material tightens its fall so the head does not read as a flat grey band on a short screen */
  .bg-material::before{
    background:
      linear-gradient(180deg,rgba(26,31,28,.68),rgba(12,15,13,.28) 40%,transparent 72%),
      radial-gradient(90% 34% at 50% -8%,rgba(171,201,176,.07),transparent 66%)}
}
/* 430 and below is where the head is genuinely small. The light tightens once more so it stays a
   source above the index rather than a haze behind the whole block, and the lattice opens again
   so a 3x screen still resolves individual marks. Tested at 320 / 375 / 390 / 430. */
@media(max-width:430px){
  .bg-atmos::before{
    background:
      radial-gradient(120% 30% at 50% -4%,rgba(171,201,176,.19),rgba(171,201,176,.05) 44%,transparent 78%),
      linear-gradient(180deg,transparent 60%,rgba(6,8,7,.54))}
  .bg-grid::before{background-size:40px 40px,40px 40px,40px 40px;
    -webkit-mask-image:radial-gradient(78% 38% at 50% 18%,#000 10%,transparent 70%);
    mask-image:radial-gradient(78% 38% at 50% 18%,#000 10%,transparent 70%)}
  .bg-steel::before{
    background:
      repeating-linear-gradient(102deg,rgba(255,255,255,.024) 0 1px,transparent 1px 6px),
      linear-gradient(180deg,rgba(19,23,21,.56),transparent 70%)}
}
@media(prefers-reduced-motion:reduce){ .rv-bg::before{animation:none!important} }

/* ============================================================================================
   INNER-PAGE INTRO
   --------------------------------------------------------------------------------------------
   Shared skeleton, per-page identity. The skeleton is: back control, a technical index that says
   WHERE you are in the site, the title, one line of support, and a metadata strip that only
   appears when there is a fact to put in it. What varies per page is the index number, the
   background surface and whether the metadata strip exists at all — which is enough to make four
   pages feel like four destinations without four layouts to maintain.

   Deliberately NOT a hero: no min-height, no viewport units. The block is exactly as tall as its
   content, so on a phone the first real content is a short scroll away instead of below a screen
   of empty graphite.
   ============================================================================================ */
.rv-ix{display:flex;align-items:center;gap:12px;font-family:var(--font-mono);font-size:11px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--faint);margin-bottom:16px}
.rv-ix .n{color:var(--sage);font-variant-numeric:tabular-nums lining-nums}
.rv-ix .sep{width:26px;height:1px;background:var(--chip-bd);flex:none}
.rv-meta{display:flex;align-items:center;gap:clamp(16px,3vw,34px);flex-wrap:wrap;margin-top:26px;
  padding-top:18px;border-top:1px solid var(--div)}
.rv-meta .m{display:flex;flex-direction:column;gap:3px}
.rv-meta .mk{font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint)}
.rv-meta .mv{font-size:15px;color:var(--text-hi);font-variant-numeric:tabular-nums lining-nums}
@media(max-width:600px){
  .rv-meta{gap:14px 22px;margin-top:20px}
  .rv-meta .mv{font-size:14px}
}

/* ---------------------------------------------------------------- page rhythm */
/* `overflow-x:clip` here, and deliberately NOT on `body`: overflow set on body PROPAGATES to the
   viewport and leaves body itself visible, so a full-bleed background surface inside the content
   rail still widened the document — measured at +544px on a 1440 screen. `main` is an ordinary
   block, so the clip applies where it is written.
   `clip` rather than `hidden` on purpose: `hidden` would make main a scroll container and break
   the `position:sticky` sidebars on the project and service pages. `clip` does not. */
main{padding-top:calc(var(--hh) + 8px);overflow-x:clip}
/* The rail is the width of the CONTENT, not of the box around it. The homepage caps its section
   children at 1200px and pads OUTSIDE that cap (.rv-section{padding:54px 64px} +
   .rv-section>*{max-width:1200px}), so its content measures 1200px. This capped the box at 1200
   and padded INSIDE it, so shell content measured 1088 - the same site, two content widths, and a
   56px horizontal jump every time a visitor moved between them. Adding the gutter to the cap makes
   both resolve to 1200px of content while narrow screens keep exactly the gutter they had. */
.rv-wrap{max-width:calc(var(--rail) + 2 * clamp(20px,4vw,56px));margin:0 auto;
  padding:0 clamp(20px,4vw,56px)}
/* Tightened 2026-08-25 (owner): the work should be on screen, not below it. The head still
   has air - it is a masthead, not a toolbar - but it no longer reserves room for the two
   label rows that used to sit above the title. */
/* The fixed header already holds the page down (main{padding-top:--hh + 8px}), so a second
   reserve here is what produced the empty band above every Back control. */
.rv-page-head{padding:clamp(14px,1.8vw,22px) 0 clamp(18px,2.4vw,26px)}
/* ---------------------------------------------------------------- back control
   Every inner page needs a way out that is part of the design rather than a browser affordance.
   The shape is a fabrication drawing's leader line: an arrow, a mono label set in the same
   register as the index numbers, and a hairline that runs from the label out to the edge of the
   text column. On hover the arrow travels the short distance a real control would, the label
   lifts to sage, and the hairline brightens — one gesture, three coordinated cues, no bounce.

   It replaces the breadcrumb on pages that have a single obvious parent; the breadcrumb stays
   where the trail is genuinely three deep. */
/* THE RETURN CONTROL.
   Rejected twice. First it was 11.5px of mono type inside 44px of invisible padding - a whisper.
   Then it became a bordered disc with the word beside it, and the owner read that as two unrelated
   controls sitting next to each other, which is fair: a circle and a word share no edge.

   It is one drawn object now. A single rule runs under the whole control - arrow and label
   together - the way a leader line on a drawing carries its annotation. The arrow is a chevron ON
   that line rather than a badge floating beside it, so there is nothing left to read as a second
   element. The rule is what gives it architectural presence without turning it into a button, and
   it is what the eye catches before it reads the word. */
.rv-back{display:inline-flex;align-items:center;gap:13px;padding:9px 4px 10px;margin-bottom:16px;
  font-family:var(--font-mono);font-size:13px;letter-spacing:.15em;text-transform:uppercase;
  color:var(--text-rev);min-height:44px;position:relative;
  transition:color var(--t-ctl) var(--ease)}
.rv-back::after{content:'';position:absolute;left:0;right:0;bottom:0;height:1px;
  background:linear-gradient(90deg,var(--sage) 0%,var(--border-ctl) 42%,transparent 100%);
  opacity:.55;transition:opacity var(--t-ctl) var(--ease)}
.rv-back:hover::after,.rv-back:focus-visible::after{opacity:1}
.rv-back .ar{width:16px;height:16px;flex:none;display:flex;align-items:center;justify-content:center;
  color:var(--sage);transition:transform var(--t-move) var(--ease)}
.rv-back .ar svg{width:100%;height:100%;display:block}
.rv-back .ln{display:none}
.rv-back:hover,.rv-back:focus-visible{color:var(--text-hi)}
.rv-back:hover .ar,.rv-back:focus-visible .ar{transform:translateX(-5px)}
.rv-back:focus-visible{outline:2px solid var(--sage);outline-offset:4px;border-radius:3px}
@media(max-width:600px){
  /* On a phone the control sits alone above the title and gets a real tap target rather than a
     4px-tall line of text. */
  .rv-back{margin-bottom:14px;font-size:12px}
  .rv-back:hover .ln{width:34px}
}

.rv-crumb{display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:11px;
  letter-spacing:.4px;color:var(--faint);margin-bottom:22px;flex-wrap:wrap}
.rv-crumb a:hover{color:var(--sage)}
.rv-crumb .sep{opacity:.45}
.rv-eyebrow{font-family:var(--font-display);text-transform:uppercase;
  letter-spacing:var(--tr-label);font-size:calc(12px * var(--sz-label));
  font-weight:var(--w-eyebrow);color:var(--sage)}
.rv-h1{font-size:clamp(26px,4.6vw,44px);letter-spacing:var(--tr-display);color:var(--text-hi);
  line-height:var(--lh-display);margin-top:6px}
.rv-eyebrow + .rv-h1{margin-top:14px}
.rv-h2{font-size:clamp(21px,3.4vw,29px);letter-spacing:var(--tr-display);color:var(--text)}
.rv-lead{color:#9CA4A0;font-size:clamp(14.5px,1.5vw,16.5px);line-height:1.68;max-width:640px;
  margin-top:18px}
/* ============================================================== THE SAGE ACCENT
   The hairline is the drawing rule: it states where one idea ends without adding a box.

   IT HAS TWO ROLES, AND ONLY ONE OF THEM WAS EVER DECLARED.

     DIVIDER  the rule opens a section whose first visible content is a grid or a list. The gap
              below it is small; the section's own padding does the separating.
     ACCENT   the rule introduces a heading directly under it. This is the one the owner means by
              "the sage line above headings", and until 2026-09-22 the distance was not declared
              here at all - it was written inline, per occurrence, in the HTML:

                process.html   h2                style="margin:30px 0 24px"
                process.html   .rv-sect-head     style="margin-top:34px"
                project.html   .rv-sect-head     style="margin-top:30px"
                projects.html  .rv-sect-head     style="margin-top:34px"
                service.html   .rv-rule          style="margin-bottom:26px"
                service.html   h2                style="...;margin:26px 0 0"
                service.html   .rv-sect-head     style="margin-top:30px"   (twice)
                services.html  .rv-sect-head     style="margin-top:34px"

              Nine declarations of one relationship - 26, 26, 30, 30, 30, 30, 34, 34, 34 - and a
              tenth here, because the phone rule below used to add `margin-bottom:22px` to the
              rule itself, which STACKED with whichever inline number the heading carried. That
              is why the same accent showed a different amount of daylight on different pages and
              did not even move in the same direction between widths: measured to the ink, the
              gap spread 14.6px across pages at 768px while spreading 3.0px at 390px.

   THE RULE, STATED ONCE. The gap belongs to the RELATIONSHIP, not to either element, so it is
   declared on the adjacency and nowhere else. `.rv-rule` itself now carries no margin at all, so
   nothing can stack with it. One token, fluid and monotonic across the range.

   The residual difference between the scripts - about 2px more daylight in Armenian and Russian
   than in English at the same box gap - is NOT corrected. That is the ascent of the script
   itself; removing it would mean giving Armenian a smaller gap than English to make a
   measurement match, which is the opposite of what the eye wants. */
:root{--rv-rule-gap:clamp(24px,1.9vw,32px);--rv-rule-flow:4px}
.rv-rule{height:1px;background:linear-gradient(90deg,var(--sage),rgba(171,201,176,.05) 42%,transparent);
  margin:0;opacity:.55}
.rv-rule + *{margin-top:var(--rv-rule-flow)}
.rv-rule + h1,.rv-rule + h2,.rv-rule + h3,
.rv-rule + .rv-sect-head{margin-top:var(--rv-rule-gap)}
/* On a phone this hairline was doing two jobs and failing the second. Full width it reads as a
   section divider, which is what it is for; but with only 4px of its own margin and 22px of
   section padding under it, it landed 26px above a 24px Armenian heading - and Armenian carries
   tall ascenders and diacritics, so the caps come up to meet it. The owner reads that as a line
   sitting on the text, and the geometry agrees even though the boxes never intersect.
   So on a phone it stops being a divider and becomes what it is actually next to: a short accent
   mark, with real air under it. 56px instead of the full column, a little quieter, and 22px of
   its own margin on top of the section padding - about 44px of clear ground under the mark.
   Desktop keeps the full-width rule; nothing was wrong with it there. */
@media(max-width:600px){
  /* The LENGTH changes on a phone - a short accent mark rather than a full-column divider - and
     that decision stands. The MARGIN does not change here any more: the 22px it used to add was
     the tenth declaration of the accent gap and the one that made the phone disagree with every
     other width. The gap comes from --rv-rule-flow / --rv-rule-gap at every size. */
  .rv-rule{width:56px;opacity:.45}
  :root{--rv-rule-flow:22px}
}
/* Section padding is per-side, so two adjacent sections stacked 72 + 72 = 144px of air. On a page
   with a lot to say that reads as architectural spacing; on a project that has a gallery and a
   call to action it reads as a page that ran out of content. Halved to ~104px between sections,
   which is still generous and no longer looks like an omission. The hairline rule and the section
   heading do the rest of the separating. */
.rv-sect{padding:clamp(30px,4.2vw,52px) 0}
/* The LAST section on a page is followed by the footer, which brings its own top margin. Left
   alone the two stack into ~130px of nothing at 1440 and considerably more on the pages that are
   currently thin — /services/<slug> measured a quarter-screen of empty graphite between its call
   to action and the footer rule, which reads as content that failed to load rather than as air.
   The section keeps its top padding and gives up its bottom one; the footer's margin is the gap. */
.rv-wrap > .rv-sect:last-child{padding-bottom:0}
/* THE SECTION HEAD IS A HEADING AND AN ACTION ON ONE LINE, so they sit on one BASELINE.

   This had three alignments live at once - `flex-end` here, `center` in product.css (which loads
   later and won), `flex-start` in product.css's phone block - and the value that won decided
   where the HEADING starts, because the row's height is set by the 44px ghost button beside it,
   not by the heading. Centred, the heading was pushed 11.6px down inside its own row, so the
   sage accent above it measured 35.6px from a composite head and 24px from a plain <h2>: the
   same accent, two distances, decided by whether the section happened to have a button.

   `baseline` takes the button out of that equation - the heading's position is now a property of
   the heading - and it is also what the two elements want typographically: a title and the link
   that acts on it, reading as one line. The phone override stays: below 600px the row wraps, and
   a wrapped row has no shared baseline to sit on. */
.rv-sect-head{display:flex;align-items:baseline;justify-content:space-between;gap:24px;
  flex-wrap:wrap;margin-bottom:clamp(22px,3vw,34px)}
/* A section that is only a rule + a heading + an empty-state note does not need a full section's
   air around it — the hairline has already done the separating. Applied by a page when it KNOWS
   the section is thin, not guessed from the markup. */
.rv-sect.tight{padding:clamp(18px,2.4vw,30px) 0}
/* The section that follows a page head sits closer to it than two sections sit to each other -
   that is the rhythm, and it is right. What was wrong is that four pages each hand-wrote it inline
   at a different value (26 / 32 / 34 / 34px), so the same relationship measured differently on
   every page. One rule, one value; the rest of the rhythm (52px between sections, 0 at the end)
   is unchanged. */
.rv-sect.rv-sect-lead{padding-top:clamp(22px,2.8vw,32px)}

/* ---------------------------------------------------------------- controls */
.rv-btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;
  font-family:var(--font-body);font-size:13px;font-weight:600;letter-spacing:.4px;
  white-space:nowrap;padding:12px 20px;border-radius:var(--r-control,2px);border:1px solid transparent;
  transition:transform var(--t-ctl) var(--ease),box-shadow var(--t-ctl) var(--ease),
  background var(--t-ctl) var(--ease),color var(--t-ctl) var(--ease),border-color var(--t-ctl) var(--ease)}
/* Every shared button clears the touch minimum on a coarse pointer. Several sat at 42px — close
   enough to look right in a screenshot, and one mis-tap away in a hand. */
/* 44px is the floor, and it has to reach every control a finger uses - not just .rv-btn. Measured
   short before this: tag chips at 40px, the ghost "All work" link at 41px and the skip link at
   42.8px. Three pixels is still a miss. */
@media (pointer:coarse){
  .rv-btn{min-height:44px}
  .skip{min-height:44px}
}
.rv-btn svg{width:14px;height:14px;flex:0 0 auto}
.rv-btn-solid{background:var(--sage);color:var(--on-sage)}
.rv-btn-solid:hover{background:var(--sage-br);transform:translateY(-2px);
  box-shadow:0 10px 26px -16px rgba(171,201,176,.42)}
.rv-btn-ghost{background:transparent;color:var(--sage);border-color:var(--ghost-bd)}
.rv-btn-ghost:hover{background:rgba(171,201,176,.1);color:var(--sage-br);border-color:var(--sage);
  transform:translateY(-2px);box-shadow:0 8px 20px -14px rgba(171,201,176,.4)}
.rv-btn:disabled{opacity:.55;pointer-events:none}
.rv-input{width:100%;background:var(--inset);border:1px solid var(--border-field);border-radius:var(--r-control,2px);
  color:var(--text);font-family:var(--font-body);font-size:14px;padding:13px 16px;outline:none;
  transition:border-color var(--t-fast),box-shadow var(--t-fast)}
.rv-input::placeholder{color:var(--faint-2)}
.rv-input:focus{border-color:var(--sage);box-shadow:0 0 0 3px rgba(171,201,176,.12);outline:none}
.rv-input.bad{border-color:var(--danger);box-shadow:0 0 0 3px rgba(207,106,106,.14)}
textarea.rv-input{resize:vertical;min-height:104px;line-height:1.55}
.rv-input:-webkit-autofill,.rv-input:-webkit-autofill:hover,.rv-input:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--text);-webkit-box-shadow:0 0 0 1000px var(--inset) inset;
  caret-color:var(--text);transition:background-color 99999s ease-in-out 0s}

/* ============================================================================================
   THE TAG SYSTEM — one owner for every visitor-facing chip
   --------------------------------------------------------------------------------------------
   THE DEFECT THIS REPLACES, measured rather than guessed (2026-08-20):
   `.rv-chip` carried `white-space:nowrap` with `max-width:none`. A chip therefore grew to
   whatever width its label needed and nothing stopped it. Probed at 320/375/390 in all three
   languages: a long Armenian label produced a 375px-wide chip inside a 300px column — right edge
   381px against a container edge of 300px, i.e. the chip ESCAPED its row and ran off the screen.
   The text itself was never clipped (scrollWidth == clientWidth), which is why "make it wrap"
   rather than "make it scroll" is the correct fix: the box was the problem, not the type.

   Today's real labels top out at 15 characters, which is why the page looks fine on this data and
   breaks on the owner's. The rule below is sized for the labels that are coming, not the ones
   that are here.

   TYPOGRAPHY TOKENS — one definition, every chip on the site, so a filter chip and a project chip
   cannot drift apart. Changing a tag's look is changing these six values.
   ============================================================================================ */
:root{
  --tag-fs:12px;          /* size */
  --tag-lh:1.35;          /* line height, sized so a two-line chip is not cramped */
  --tag-ls:.3px;          /* tracking */
  --tag-px:14px;          /* horizontal padding */
  --tag-py:7px;           /* vertical padding */
  --tag-gap:9px;          /* gap between chips in a row */
}
/* Armenian and Russian are wider scripts; a chip is small enough that the tracking is what makes
   the difference between "compact" and "cramped". */
html[lang="hy"]{--tag-ls:0;--tag-fs:12.5px}
html[lang="ru"]{--tag-ls:.1px}

.rv-chip{display:inline-flex;align-items:center;gap:7px;
  padding:var(--tag-py) var(--tag-px);border-radius:999px;
  border:1px solid var(--chip-bd-on);background:var(--sage-chip);color:var(--chip-tx);
  font-size:var(--tag-fs);line-height:var(--tag-lh);letter-spacing:var(--tag-ls);
  /* THE FIX, three rules that work together:
       max-width  a chip can never be wider than the row it sits in, at any label length;
       normal     it wraps to a second line instead of growing sideways for ever;
       break-word a single unbroken compound (Armenian does this) breaks rather than escaping. */
  max-width:100%;
  white-space:normal;
  overflow-wrap:anywhere;
  text-align:left;
  transition:border-color var(--t-ctl),color var(--t-ctl),background var(--t-ctl),
  transform var(--t-ctl) var(--ease)}
/* the count on a filter chip stays on the first line and never wraps away from its label */
.rv-chip .rv-idx{flex:0 0 auto;white-space:nowrap}
button.rv-chip:hover,a.rv-chip:hover{border-color:var(--ghost-bd);color:var(--text-hi);transform:translateY(-1px)}
/* Selected state is deliberately NOT colour alone: aria-pressed carries it for assistive tech,
   and the weight and the filled ground carry it visually, so it survives colour blindness and a
   greyscale screenshot both. */
button.rv-chip[aria-pressed="true"]{background:var(--sage);border-color:var(--sage);
  color:var(--on-sage);font-weight:600}
.rv-chips{display:flex;flex-wrap:wrap;gap:var(--tag-gap);min-width:0}


/* ---- APPROVED TAG STYLES ------------------------------------------------------------------
   Six material tokens, not free colour. A tag picks one of these in Admin; there is no hex field
   anywhere, because a per-tag colour picker is how a graphite-and-sage site becomes a bag of
   sweets. Every one of them is a dark ground with a light label — the same object in a different
   metal — and each was checked for contrast against its own background, not against the page.

   sage      the Ravenor accent. The default, and the only one that uses the brand colour.
   steel     cool bright metal. For structural / load-bearing categories.
   graphite  the quiet one. For categories that are context rather than capability.
   warm      oxidised / coated metal. For finish and decorative categories.
   muted     no ground at all. For low-salience categories.
   outline   drawn, not filled. For a category that is a boundary rather than a thing.
   -------------------------------------------------------------------------------------------- */
.rv-chip.tag-sage    {background:#15201A;border-color:#2C3A30;color:#C5D0C6}
.rv-chip.tag-steel   {background:#151A1D;border-color:#2E373C;color:#C4CBCE}
.rv-chip.tag-graphite{background:#131617;border-color:#262B2D;color:#A7AEAB}
.rv-chip.tag-warm    {background:#1B1714;border-color:#3A3129;color:#D6C6B0}
.rv-chip.tag-muted   {background:transparent;border-color:#242A2C;color:#98A09C}
.rv-chip.tag-outline {background:transparent;border-color:#4C6654;color:#A9C3AE}
a.rv-chip.tag-sage:hover,button.rv-chip.tag-sage:hover        {border-color:#5A7A64;color:#E4EDE5}
a.rv-chip.tag-steel:hover,button.rv-chip.tag-steel:hover      {border-color:#4A585E;color:#E8EDEF}
a.rv-chip.tag-graphite:hover,button.rv-chip.tag-graphite:hover{border-color:#3A4245;color:#DCE2E0}
a.rv-chip.tag-warm:hover,button.rv-chip.tag-warm:hover        {border-color:#584A3C;color:#EFE2CF}
a.rv-chip.tag-muted:hover,button.rv-chip.tag-muted:hover      {border-color:#39413F;color:#D2D8D5}
a.rv-chip.tag-outline:hover,button.rv-chip.tag-outline:hover  {border-color:#7FA588;color:#CFE3D3}
/* Selected always wins over the style token: one unmistakable "this filter is on" appearance
   across every tag, rather than six different selected states. */
button.rv-chip[aria-pressed="true"]{background:var(--sage)!important;border-color:var(--sage)!important;
  color:var(--on-sage)!important}
@media(max-width:400px){
  /* the smallest screens get slightly tighter chips so three short tags still make one row */
  :root{--tag-px:12px;--tag-gap:7px}
}

/* ---------------------------------------------------------------- cards */
.rv-card{background:linear-gradient(180deg,var(--raised),var(--surface));border:1px solid var(--border);
  border-radius:13px;position:relative;overflow:hidden;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
  transition:transform var(--t-move) var(--ease),border-color var(--t-move),box-shadow var(--t-move)}
a.rv-card:hover,.rv-card.hoverable:hover{transform:translateY(-3px);border-color:var(--chip-bd);
  box-shadow:0 18px 40px -28px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.06)}
.rv-ico{width:28px;height:28px;color:var(--sage);display:inline-block}
.rv-ico svg{width:100%;height:100%;display:block}
/* The index number is a drawing annotation: tabular so a column of them lines up exactly. */
.rv-idx{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;color:var(--faint);
  font-variant-numeric:tabular-nums lining-nums}

/* media frame — one shared aspect box, so a grid never jumps as images arrive */
.rv-frame{position:relative;overflow:hidden;background:var(--inset);
  background-image:linear-gradient(115deg,rgba(255,255,255,.02) 0 18%,transparent 18% 100%)}
.rv-frame img{width:100%;height:100%;object-fit:cover;transition:transform 1.1s var(--ease),opacity var(--t-move) var(--ease)}
/* <picture> is an inline wrapper; without this the mobile-crop variant collapses to its intrinsic
   size inside the aspect box instead of filling it. */
.rv-frame picture{display:block;width:100%;height:100%}
/* a video's poster carries a play mark, so a still that is really a film says so */
.rv-playmark{position:absolute;left:50%;top:50%;width:46px;height:46px;margin:-23px 0 0 -23px;
  z-index:3;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:rgba(10,13,11,.66);border:1px solid var(--ghost-bd);color:var(--sage);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);pointer-events:none;
  transition:transform var(--t-ctl) var(--ease),background var(--t-ctl) var(--ease)}
.rv-playmark svg{width:18px;height:18px;margin-left:2px}
a.rv-card:hover .rv-playmark{transform:scale(1.08);background:rgba(10,13,11,.8)}
/* a video row whose poster has not been uploaded yet */
.rv-noposter{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  color:var(--border-ctl)}
.rv-noposter svg{width:44px;height:44px}
a.rv-card:hover .rv-frame img{transform:scale(1.045)}
/* Project photographs are 1.7-2.2 MB PNGs, so on any real connection there is a visible period
   where the frame is empty and then the image POPS in at full opacity. Two frames of black
   followed by a hard cut is the cheapest-looking thing a gallery can do.
   `img[data-rvimg]` starts at zero and shell.js flips `.ready` on decode, so the picture resolves
   out of the graphite instead of replacing it. A frame whose image is already in cache decodes
   synchronously and lands ready on the first paint, so nothing fades that did not need to. */
.rv-frame img[data-rvimg]{opacity:0}
.rv-frame img[data-rvimg].ready{opacity:1}
/* the drawing mark that occupies a frame while it waits, and stays where there is no picture */
.rv-frame::before{content:'';position:absolute;left:50%;top:50%;width:26px;height:26px;
  margin:-13px 0 0 -13px;border:1px solid var(--border-ctl);border-radius:2px;opacity:.5;
  transition:opacity var(--t-move) var(--ease)}
.rv-frame:has(img.ready)::before,.rv-frame.ph::before{opacity:0}
.rv-frame.ph::after{content:'';position:absolute;inset:0;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.045),transparent);
  transform:translateX(-100%);animation:rv-shimmer 1.5s infinite}
@keyframes rv-shimmer{100%{transform:translateX(100%)}}
@media(prefers-reduced-motion:reduce){
  /* a fade is decoration; the picture is the information. Never withhold it. */
  .rv-frame img[data-rvimg]{opacity:1}
}

/* ============================================================================================
   SPECIFICATION STRIP
   --------------------------------------------------------------------------------------------
   A ruled row of key/value pairs, set the way a fabrication drawing sets its title block: mono
   keys, tabular values, hairline separators, no boxes. It is how a project or a service states
   FACTS — and the rule is that it only ever states facts that exist in the database. A page with
   one real fact shows one cell; a page with none does not render the strip at all. There is no
   filler cell, because a specification with an invented row is not a specification.
   ============================================================================================ */
.rv-spec{display:flex;flex-wrap:wrap;align-items:stretch;gap:0;margin-top:clamp(16px,2.2vw,22px);
  border-top:1px solid var(--div);border-bottom:1px solid var(--div)}
.rv-spec .cell{display:flex;flex-direction:column;gap:5px;padding:14px clamp(16px,2.4vw,26px) 14px 0;
  margin-right:clamp(16px,2.4vw,26px);border-right:1px solid var(--div);min-width:0}
.rv-spec .cell:last-child{border-right:0;margin-right:0;padding-right:0}
.rv-spec .k{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap}
.rv-spec .v{font-size:14.5px;color:var(--text-hi);line-height:1.4;
  font-variant-numeric:tabular-nums lining-nums}
.rv-spec .v.sage{color:var(--sage)}
@media(max-width:600px){
  /* On a phone a five-cell row becomes five one-word columns. Two per row keeps the values
     readable and keeps the ruled-block character; the right-hand border goes so the second
     column does not carry a stray hairline into the margin. */
  .rv-spec{display:grid;grid-template-columns:1fr 1fr;gap:0}
  .rv-spec .cell{padding:12px 12px 12px 0;margin-right:0;border-right:1px solid var(--div);
    border-bottom:1px solid var(--div)}
  .rv-spec .cell:nth-child(2n){border-right:0;padding-left:12px}
  .rv-spec .cell:nth-last-child(-n+2){border-bottom:0}
  .rv-spec .cell:last-child{padding-right:12px}
  .rv-spec .v{font-size:13.5px}
}

/* ============================================================================================
   THE CAPABILITY SPINE — MEASURE / ESTIMATE / DRAW / FABRICATE / INSTALL
   --------------------------------------------------------------------------------------------
   The five things Ravenor actually performs, in order. It already exists inside the Quote panel
   as a compressed strip; this is the same five keys given room, so the site says the same thing
   in the same words wherever it says it. It is a claim that cannot be wrong, which is the entire
   reason it is the trust device instead of a rating or a project count.

   Set as a measured run: a hairline through the whole span, a mark at each stage, tabular index.
   The mark is a square rotated 45 degrees — a setting-out point on a drawing, not a bullet.
   ============================================================================================ */
.rv-spine{position:relative;display:grid;
  grid-template-columns:repeat(5,minmax(0,1fr));gap:0;margin-top:clamp(20px,2.6vw,28px);
  padding-top:22px}
.rv-spine::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;
  background:linear-gradient(90deg,var(--sage),rgba(171,201,176,.20) 34%,rgba(171,201,176,.10) 70%,transparent)}
.rv-spine .st{position:relative;padding:0 10px 0 0;min-width:0}
.rv-spine .st::before{content:'';position:absolute;top:-25px;left:0;width:6px;height:6px;
  background:var(--base);border:1px solid var(--chip-bd);transform:rotate(45deg)}
.rv-spine .st.on::before{border-color:var(--sage);background:var(--sage)}
.rv-spine .n{display:block;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.2em;
  color:var(--faint);font-variant-numeric:tabular-nums lining-nums;margin-bottom:5px}
.rv-spine .t{display:block;font-family:var(--font-display);font-size:calc(11.5px * var(--sz-label));
  letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--text-rev);line-height:1.35;
  overflow-wrap:break-word}
.rv-spine .st.on .t{color:var(--sage)}
@media(max-width:760px){
  /* Five uppercase words across a 390px screen is five columns of two letters. The spine turns
     vertical and keeps its rule down the left, which is the same drawing, rotated. */
  .rv-spine{grid-template-columns:1fr;padding-top:0;padding-left:20px;gap:0}
  .rv-spine::before{top:4px;bottom:4px;left:2px;right:auto;width:1px;height:auto;
    background:linear-gradient(180deg,var(--sage),rgba(171,201,176,.16) 40%,transparent)}
  .rv-spine .st{display:flex;align-items:baseline;gap:10px;padding:9px 0}
  .rv-spine .st::before{top:15px;left:-21px}
  .rv-spine .n{margin-bottom:0}
}

/* ============================================================================================
   DIRECTION-AWARE HOVER
   --------------------------------------------------------------------------------------------
   The pointer crosses an edge; the response enters from THAT edge and leaves by the edge the
   pointer leaves through. It is the one interaction on the site that acknowledges where you came
   from, which is why it is reserved for the work — a project card is a thing you approach.

   The Ravenor reading of it is not a coloured tile sliding over a photograph. It is a machinist's
   response: a graphite veil that lets the picture through, and a single sage hairline that
   travels the crossed edge. Nothing bounces, nothing tints the photograph green.

   shell.js sets `--ex`/`--ey` to the entry vector (-1, 0 or 1 per axis) on pointerenter and to the
   exit vector on pointerleave; CSS does the rest, so there is no per-frame work anywhere.
   ============================================================================================ */
.rv-daw{--ex:0;--ey:1}
.rv-daw .rv-daw-veil{position:absolute;inset:0;z-index:2;pointer-events:none;opacity:0;
  background:linear-gradient(180deg,rgba(8,11,9,.10),rgba(8,11,9,.62));
  transform:translate3d(calc(var(--ex) * 26%),calc(var(--ey) * 26%),0);
  transition:opacity var(--t-move) var(--ease),transform var(--t-move) var(--ease-mech)}
.rv-daw .rv-daw-edge{position:absolute;z-index:3;pointer-events:none;background:var(--sage);
  opacity:0;transition:opacity var(--t-ctl) var(--ease),transform var(--t-move) var(--ease-line)}
/* the hairline sits on whichever axis was crossed and runs the full span of that edge */
.rv-daw .rv-daw-edge{left:0;right:0;height:1px;top:0;
  transform:translate3d(calc(var(--ex) * -100%),0,0)}
.rv-daw[data-edge="bottom"] .rv-daw-edge{top:auto;bottom:0}
.rv-daw[data-edge="left"]  .rv-daw-edge,
.rv-daw[data-edge="right"] .rv-daw-edge{top:0;bottom:0;height:auto;width:1px;
  left:auto;right:auto;transform:translate3d(0,calc(var(--ey) * -100%),0)}
.rv-daw[data-edge="left"]  .rv-daw-edge{left:0}
.rv-daw[data-edge="right"] .rv-daw-edge{right:0}
.rv-daw.daw-on .rv-daw-veil{opacity:1;transform:none}
.rv-daw.daw-on .rv-daw-edge{opacity:1;transform:none}
/* The caption rides in behind the veil. It carries the affordance — "View project" — which is
   the thing a hover on a picture should actually produce. */
.rv-daw .rv-daw-in{position:absolute;left:0;right:0;bottom:0;z-index:4;pointer-events:none;
  padding:clamp(14px,2vw,20px);display:flex;align-items:flex-end;justify-content:space-between;
  gap:14px;opacity:0;transform:translate3d(0,10px,0);
  transition:opacity var(--t-move) var(--ease),transform var(--t-move) var(--ease-mech)}
.rv-daw.daw-on .rv-daw-in{opacity:1;transform:none}
.rv-daw .rv-daw-go{display:inline-flex;align-items:center;gap:8px;color:var(--sage);font-size:12.5px;
  font-weight:600;letter-spacing:.3px;white-space:nowrap}
.rv-daw .rv-daw-go svg{width:14px;height:14px}
/* Touch has no hover and no direction, so on a phone this interaction is simply INERT — not
   "always on". The first attempt left the veil and its label permanently visible, which put a
   second, dimmer "View project" over every photograph directly above the real one in the card
   body: the same words twice, once hard to read. A card on touch already carries its affordance
   in its own foot, and any composition that sets type over a photograph brings its own fall-off,
   so nothing here is load-bearing. It is removed rather than reduced. */
@media(hover:none),(pointer:coarse){
  .rv-daw .rv-daw-veil,.rv-daw .rv-daw-in,.rv-daw .rv-daw-edge{display:none}
}
@media(prefers-reduced-motion:reduce){
  .rv-daw .rv-daw-veil,.rv-daw .rv-daw-in{transform:none!important}
}

/* Empty states come in two weights.

   `.rv-note` is the quiet one: a hairline-led line of text where a section has nothing to show
   yet. The large dashed box it replaces was a dashboard convention - it drew MORE attention to
   the absence than the section would have had with content in it.

   `.rv-cta-band` is the loud one, and it is not really an empty state at all: where a page has
   run out of things to say about the work, the right thing to put there is an invitation, not a
   table of facts the visitor can already see. */
.rv-note{display:flex;align-items:flex-start;gap:14px;padding:18px 0 4px;
  border-top:1px solid var(--div);color:var(--muted);font-size:14px;line-height:1.65;max-width:640px}
.rv-note .m{width:16px;height:16px;flex:0 0 auto;color:var(--faint);margin-top:4px}
.rv-note .m svg{width:100%;height:100%}
.rv-note a{color:var(--sage);white-space:nowrap}
.rv-note a:hover{color:var(--sage-br)}

.rv-cta-band{display:grid;grid-template-columns:1fr auto;gap:clamp(20px,3vw,40px);align-items:center;
  border:1px solid var(--border);border-radius:var(--r-surface,12px);padding:clamp(24px,3.2vw,38px);
  /* The same green-black ground the full-bleed closing band uses on the list pages, so a visitor
     moving from /services into a service detail meets ONE closing language rather than two. The
     green lives in the TONE; the only literal brand colour here is the lit top edge. */
  background:
    radial-gradient(96% 130% at 50% 0%,rgba(171,201,176,.05),rgba(171,201,176,0) 64%),
    linear-gradient(180deg,#0C120E,#0A0F0C);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.03);position:relative;overflow:hidden}
/* the same machined hairline the service cards use, so the band belongs to the set */
.rv-cta-band::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;
  background:linear-gradient(90deg,var(--sage),rgba(171,201,176,.06) 46%,transparent)}
.rv-cta-band h2{font-size:clamp(19px,2.4vw,25px);color:var(--text-hi);letter-spacing:var(--tr-display)}
.rv-cta-band p{color:var(--muted);font-size:14.5px;line-height:1.65;margin-top:11px;max-width:520px}
.rv-cta-acts{display:flex;flex-direction:column;gap:10px;min-width:210px}
@media(max-width:760px){
  .rv-cta-band{grid-template-columns:1fr}
  .rv-cta-acts{min-width:0}
}

.rv-empty{border:1px dashed var(--border-ctl);border-radius:var(--r-surface,12px);padding:44px 28px;text-align:center;
  color:var(--muted);font-size:14px}
.rv-err{border:1px solid rgba(207,106,106,.4);background:rgba(207,106,106,.07);border-radius:10px;
  padding:16px 20px;color:#e3a6a6;font-size:13.5px;margin-bottom:20px}

/* ---------------------------------------------------------------- footer */
.rv-foot{border-top:1px solid var(--div);margin-top:clamp(40px,6vw,80px);padding:clamp(38px,5vw,60px) 0 30px}
.rv-foot-grid{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:clamp(24px,4vw,56px)}
.rv-foot h2{font-family:var(--font-display);font-size:11px;letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--text-rev);margin-bottom:16px}
.rv-foot a,.rv-foot p{color:var(--muted);font-size:13.5px;line-height:1.9}
.rv-foot-col a{display:block;width:fit-content;
  transition:color var(--t-fast),transform var(--t-ctl) var(--ease)}
.rv-foot-col a:hover{color:var(--sage);transform:translateX(3px)}
.rv-foot-lockup{display:inline-flex;gap:var(--brand-mark-gap,.187em);margin-bottom:16px}
.rv-foot-lockup img{width:auto;height:var(--brand-mark-h,1.872em);aspect-ratio:var(--brand-mark-aspect,552 / 763);
  flex:none;object-fit:contain;filter:drop-shadow(0 0 14px rgba(171,201,176,.24))}
/* The 14px used to sit on the WORD. Once the lockup centres its two children, a bottom margin on
   one of them shifts that child up by half of it - 7px against a 6.87px optical drop, so the drop
   cancelled out and the footer alone lost the alignment every other surface had. Spacing below the
   lockup is the lockup's business, so it moved to the container. */
.rv-foot-word{font-family:var(--font-brand);font-size:22px;letter-spacing:var(--tr-brand);
  color:var(--text-hi);display:block}
.rv-foot-dir a{display:flex;gap:9px;align-items:flex-start;margin-top:12px}
.rv-foot-dir .d{color:var(--faint);font-size:12.5px}
.rv-foot-hours{margin-top:12px;font-family:var(--font-mono);font-size:11.5px;letter-spacing:.3px;
  color:var(--faint-2);line-height:1.9}
.rv-foot-hours .hk{display:inline-block;margin-right:.65em;color:var(--muted)}
.rv-foot-hours .hv{display:inline-block;white-space:nowrap;color:var(--text-hi)}
.rv-foot-dir svg{width:15px;height:15px;color:var(--sage);flex:0 0 auto;margin-top:4px}
/* The legal row used to be space-between: copyright hard left, legal links hard right. Ravenor
   configures exactly ONE legal link, so the right-hand side held a single item stranded across
   the full width of the page - the orphan the owner pointed at. A legal line is one sentence of
   small print, not a two-column layout, so it now reads as one run from the left with a quiet
   separator. It still works if a second link is ever configured; it simply stops reserving a
   column for a link that may not exist. */
.rv-foot-bot{display:flex;justify-content:flex-start;align-items:baseline;gap:0 18px;flex-wrap:wrap;
  border-top:1px solid var(--div);margin-top:34px;padding-top:22px;
  font-family:var(--font-mono);font-size:11px;letter-spacing:.3px;color:var(--faint)}
@media(max-width:820px){.rv-foot-grid{grid-template-columns:1fr 1fr}
  .rv-foot-brandcol{grid-column:1/-1}}
@media(max-width:520px){.rv-foot-grid{grid-template-columns:1fr}}
@media(max-width:820px){.rv-foot-cell:has(.rv-foot-hours){grid-column:1/-1}}
@media(max-width:520px){.rv-foot-hours{display:flex;align-items:baseline;flex-wrap:wrap;gap:2px 10px;font-size:12px}
  .rv-foot-hours .hk{margin-right:0}}


/* toast */
.rv-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%) translateY(18px);z-index:400;
  background:rgba(16,20,18,.96);border:1px solid var(--sage);border-radius:12px;padding:14px 22px;
  color:#fff;font-size:14px;display:flex;align-items:center;gap:10px;
  box-shadow:0 14px 44px rgba(0,0,0,.5),0 0 0 1px rgba(171,201,176,.18);opacity:0;pointer-events:none;
  transition:opacity var(--t-mass) var(--ease),transform var(--t-mass) var(--ease-mech);
  backdrop-filter:blur(8px);max-width:90vw;text-align:center}
.rv-toast.on{opacity:1;transform:translateX(-50%)}
.rv-toast.bad{border-color:var(--danger);box-shadow:0 14px 44px rgba(0,0,0,.5),0 0 0 1px rgba(207,106,106,.2)}

/* reveal-on-scroll. The transform is small on purpose — content arriving from far away reads as a
   slideshow, not as a page. */
.rv-rise{opacity:0;transform:translateY(14px);
  transition:opacity var(--t-mass) var(--ease),transform var(--t-mass) var(--ease-mech)}
.rv-rise.in{opacity:1;transform:none}

body.rv-locked{overflow:hidden}

/* Respect the visitor's setting. Everything above is decoration; none of it is information, so
   all of it can be switched off without the page losing meaning. */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;scroll-behavior:auto!important}
  .rv-rise{opacity:1;transform:none}
  .rv-frame.ph::after{animation:none}
}

/* ==============================================================================================
   TOUCH LEGIBILITY PASS
   Measured on real viewports at 320 / 375 / 390 / 430, not assumed. Every rule below fixes a
   control that was reachable with a mouse but small under a thumb. Nothing changes on desktop.
   ============================================================================================== */
@media(pointer:coarse){
  /* footer column links measured 26px tall — a stack of 26px targets a few pixels apart */
  .rv-foot-col a{padding:10px 0;min-height:44px;display:flex;align-items:center}
  /* 44, not 40. These three were raised to 40 in an earlier pass while .rv-btn, .rv-back and the
     footer links in this same block all sit at 44 - so the product had two touch floors and the
     lower one applied to chips, which are links in dense rows where a miss hits the neighbour.
     One floor, and it is the one the rest of the site already uses. */
  .nav-quote{min-height:44px;display:inline-flex;align-items:center}
  .rv-chip{min-height:44px}
  .rv-note a{display:inline-block;padding:9px 0;min-height:44px}
  .rv-sect-head .rv-btn-ghost,a.rv-btn{min-height:44px;display:inline-flex;align-items:center}
}

/* ============================================================== SOCIAL - ONE CANONICAL COMPONENT
   The owner's report was that the marks render at different sizes depending on the route. That is
   the symptom of a component whose geometry is assembled from whatever CSS happens to reach it,
   and this file alone had TWO full definitions of the strip fighting each other - a 42px ringed
   version and a 44px ring-less one - with a third set of numbers in a narrow-viewport override.
   Whichever won was an accident of source order rather than a decision.

   So the geometry is now stated ONCE, as tokens, and the component reads them. A route cannot
   change the size of a social mark without editing these four values, which is the point.

   The TREATMENT is recovered from production, which the owner showed and explicitly approved:
   an equal circular outline, one glyph scale inside it, calm equal spacing, a light mark on the
   dark field, and on approach the circle FILLS with the brand green and the mark goes dark. A
   previous pass here deleted those circles on the argument that "a row of bare marks is more
   Ravenor than a row of outlined circles". That was a reasonable opinion and it was not the
   owner's; the owner has now asked for the production treatment by name, so it comes back.

   --soc-size is the circle AND the tap target, deliberately the same number so the two can never
   drift apart into the "visible 19px / target 44px" split this component has already been through.
   44px satisfies the touch minimum at every width, and the 18px glyph inside it reproduces
   production's 40/16 fill almost exactly. Only channels the owner has configured are rendered -
   see socialHtml() in shell.js - so three real accounts render as three, never padded out. */
/* ------------------------------------------------------------------------- THE RADIUS SCALE
   Ten different corner radii were in use across the controls of one product - 5, 7, 8, 9, 10, 12,
   14, 50%, 99px and 0 - which is not a scale, it is an accumulation. Three values, each with a
   job, and every control says which family it belongs to:

     --r-control  actions and fields. 2px, not 7. A fabricator's plate is CUT, not moulded, and a
                  7px radius on a 54px button is the default shape of every SaaS product shipped
                  in the last decade - which is precisely the "generic rounded rectangle" the
                  owner named. A chamfer was prototyped against the real button and rejected: it
                  reads tech/gaming, which the brief rules out by name.
     --r-surface  things that hold an image or a block of content.
     --r-pill     chips. Deliberately a different family: a chip is a token you SELECT, not an
                  action you press, and the shape is what says so.
   Circles (the social marks, the Assistant trigger) stay at 50%; they are their own geometry. */
:root{
  /* 2px -> 6px, on the owner's call. The note above argues for a cut plate over a moulded one
     and that reasoning still holds at 6: it is half the surface radius and a fifteenth of a
     pill, so a button is still visibly a different family from a chip, and nothing here
     approaches the 7-14px SaaS default the note was written against. What changed is that
     2px reads as a right angle at a glance on a phone, which is what the owner kept seeing.
     One token, 29 consumers, so the softening lands on buttons, fields, the language menu
     and the menu's own controls at once rather than as 29 separate opinions. */
  --r-control:6px;
  --r-surface:12px;
  --r-pill:999px;

  /* --------------------------------------------------------------- THE CONTROL SCALE
     Measured before it was written, because a consolidation done against a remembered
     number is done blind. Every `.rv-btn` on the public site rendered at one of FIVE
     heights - 50, 51, 52, 54 and 59 - and the causes were three different things, only two
     of which were reasons:

       50  the base. `min-height:50px` with `15px 24px`.
       52  the subscribe button, matching the height of the FIELD beside it. A real reason,
           and now a shared token, so the pair cannot drift apart the next time either moves.
       54  the home contact band's single largest call to action. A deliberate emphasis step.
       46  an override on the contact page that sat BELOW the base, so it changed nothing and
           only made the cascade harder to read. Removed.
       51  an "Add files" trigger picking up `.cr-form label`, so a button rendered at 12px in
           spaced capitals. A defect, fixed at its source.

     Two heights with a stated reason, plus the touch floor. Not five. */
  --ctl-h:50px;          /* the button */
  --ctl-h-lg:54px;       /* one step up, for a page's single biggest action */
  --ctl-h-field:52px;    /* a button standing beside an input, matching its height */
  --ctl-h-min:44px;      /* the touch floor, and the height of a utility control */
  --ctl-pad-x:24px;

  /* --------------------------------------------------------------- THE ICON SYSTEM
     Nine literal `stroke-width` values were spread across five JavaScript files - 1.5, 1.6,
     1.7, 1.8 and 2 - so a single page drew four different line weights. Declaring it here and
     applying it in CSS beats editing nine string literals: a CSS property overrides the
     presentation attribute, so every call site is corrected at once and a tenth one written
     tomorrow inherits the system instead of adding to the pile.

     1.7 is chosen because it is what the two most considered sets already use - the service
     marks in icons.js and the operator navigation - so most of the product does not move. */
  --ico-stroke:1.7;
  /* The size scale, derived from what is actually on the page rather than invented. Nine
     rendered box sizes were measured - 18 (28 marks), 20 (13), 16 (10), 15 (8), 11 (8),
     38 (8), 22 (7), 19 (5), 26 (1) - and the biggest group is 18, so 18 is the default and
     does NOT move: resizing every button mark and every social glyph to win a rounder number
     would be churn a visitor pays for and nobody asked for.

     What moves is the drift only: 17 and 19 become 18, 15 becomes 16, 11 becomes 12, 22
     becomes 20. Nine sizes become five, and every one of the five is a different KIND of
     object rather than a different opinion about the same one. */
  --ico-xs:12px;         /* a mark inside a chip or a compact control */
  --ico-sm:16px;         /* a close or disclosure mark, smaller than the type beside it */
  --ico-md:18px;         /* the default: a mark set beside a line of type */
  --ico-lg:20px;         /* a standalone affordance the eye has to find */
  --ico-xl:26px;         /* a result mark, seen once, after something has happened */
  --ico-2xl:38px;        /* the service mark on a card, which IS the content */
}

/* One stroke, one cap, one join, on every mark in the product. `data-rv-raw` is the escape
   hatch for anything that must keep its own drawing, so the rule never has to be weakened. */
svg:not([data-rv-raw]){
  stroke-width:var(--ico-stroke);
  stroke-linecap:round;
  stroke-linejoin:round;
}
:root{
  --soc-size:44px;     /* outer circle, and the hit target - one number for both */
  --soc-glyph:18px;    /* visible mark: 41% fill, the production proportion */
  --soc-gap:12px;      /* production's spacing */
}
/* ONE OPTICAL SIZE PER MARK (owner correction, 2026-09-23; R22_v3). The marks are the brands' own
   silhouettes (simple-icons 16.32.0 - see shell.js), and they carry very different amounts of
   ink: LinkedIn's solid square covers 74% of its 24px box and Telegram's disc 67%, against 30%
   for Viber's outline and 34% for WhatsApp's. At one shared 18px the solid marks read twice as
   heavy as the outline ones beside them.

   R22_v3 changed two things. The Viber path was damaged and is now the official one (lighter:
   30% ink, not the 36% the broken fill gave it). And every size is now a WHOLE, EVEN pixel. The
   ring's inside is 42px (44 minus its 1px border), so an even mark centres on whole pixels; the
   17, 17.5, 19 and 19.5px of R22_v2 sat half a pixel off, and every straight edge - LinkedIn's
   square, YouTube's frame, Instagram's outline - rendered as two grey half-pixels instead of one
   sharp one. Chosen by eye at 1x, 1.25x, 1.5x and 2x from six measured candidates: the square at
   16 and the discs at 18 carry the same area, the outline messengers share 20. */
.rv-soc[data-brand=linkedin]{--soc-glyph:16px}
.rv-soc[data-brand=telegram]{--soc-glyph:18px}
.rv-soc[data-brand=facebook]{--soc-glyph:18px}
.rv-soc[data-brand=youtube]{--soc-glyph:18px}
.rv-soc[data-brand=instagram]{--soc-glyph:18px}
.rv-soc[data-brand=viber]{--soc-glyph:20px}
.rv-soc[data-brand=tiktok]{--soc-glyph:20px}
.rv-soc[data-brand=whatsapp]{--soc-glyph:20px}
.rv-foot-social{display:flex;flex-wrap:wrap;gap:var(--soc-gap,12px);margin-top:26px;margin-left:0}
/* `.rv-foot-col a` sets padding and min-height on every link in that column and outranks a bare
   `.rv-soc`, so the strip is addressed through its own parent as well as on its own - either
   placement gets identical geometry. */
.rv-foot-social .rv-soc,
a.rv-soc{
  display:inline-grid;place-items:center;box-sizing:border-box;flex:none;padding:0;
  width:var(--soc-size,44px);height:var(--soc-size,44px);
  min-width:var(--soc-size,44px);min-height:var(--soc-size,44px);
  border:1px solid var(--border-ctl);border-radius:50%;
  background:transparent;color:var(--muted);
  transition:color var(--t-ctl) var(--ease),border-color var(--t-ctl) var(--ease),
             background var(--t-ctl) var(--ease),transform var(--t-ctl) var(--ease)}
.rv-foot-social .rv-soc svg,
a.rv-soc svg{width:var(--soc-glyph,18px);height:var(--soc-glyph,18px);display:block}
.rv-foot-social .rv-soc:hover,.rv-foot-social .rv-soc:focus-visible,
a.rv-soc:hover,a.rv-soc:focus-visible{
  background:var(--sage);border-color:var(--sage);color:var(--on-sage);transform:translateY(-3px)}
.rv-foot-social .rv-soc:focus-visible,a.rv-soc:focus-visible{
  outline:2px solid var(--sage);outline-offset:3px}
@media(prefers-reduced-motion:reduce){
  .rv-foot-social .rv-soc,a.rv-soc{transition:none}
  .rv-foot-social .rv-soc:hover,a.rv-soc:hover{transform:none}
}


/* The separator is a BORDER on the links container, not a pseudo-element inside it: the container
   is a flex row, so a ::before becomes a flex ITEM and sits on its own baseline away from the link
   it is supposed to be separating. On a phone the row stacks, and a vertical rule between stacked
   lines would be nonsense, so it is drawn only once the row is actually side by side. */
.rv-foot-legal-links{align-items:baseline}
@media(min-width:561px){
  .rv-foot-legal-links{border-left:1px solid var(--border-ctl);padding-left:18px}
}
/* Between 521 and 560 neither the side-by-side rule above nor product.css's stacking rule
   applies, so the row wraps on its own and lands the links under the copyright. Same outcome as
   the stacked case, same fix, stated for the band that would otherwise fall between the two.
   align-items is restated because `baseline` means nothing once the axis is vertical. */
@media(max-width:560px){
  .rv-foot-bot{flex-direction:column-reverse;align-items:flex-start}
}

/* A focused invitation that explains the external step before opening Google. */
.rv-review{margin:auto;width:min(520px,calc(100% - 32px));max-height:calc(100dvh - 32px);overflow:auto;padding:28px;background:var(--surface-1,#0E1110);color:var(--text-hi,#F3F6F5);border:1px solid var(--border-2,#23282A);border-radius:12px;box-shadow:0 24px 90px #0009;font-family:var(--font-body)}
.rv-review::backdrop{background:#000b}
.rv-review-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px}
.rv-review-brand{display:flex;gap:var(--brand-mark-gap);font:700 16px Orbitron,sans-serif;letter-spacing:.1em}
.rv-review-brand img{width:auto;height:var(--brand-mark-h);aspect-ratio:var(--brand-mark-aspect,552 / 763)}
.rv-review-brand span{position:static}
.rv-review .rv-x{min-width:44px;min-height:44px;font-family:inherit;font-size:12px}
.rv-review h2{font:500 clamp(24px,5vw,32px)/1.2 var(--font-body);letter-spacing:-.025em;margin-bottom:18px}
.rv-review p{font-size:15px;line-height:1.7;color:var(--muted);margin-bottom:22px}
.rv-review .rv-review-note{font-size:12px;margin:18px 0 0}
.rv-review .rv-btn{display:flex;justify-content:center;min-height:48px;white-space:normal;text-align:center}
.reviews-write{font-family:inherit;cursor:pointer}

/* ---------------------------------------------------------------------------- the one action
   R30_v3 (owner review, 2026-09-23). The dialog used to hold a composer - write here, copy,
   paste on Google - and the owner rejected writing a review twice. What is left is the
   invitation and one action that opens Google's own composer, so the rhythm is the intro, the
   action directly under it, and one quiet line saying where Google opens. */
.rv-review-actions{display:flex;align-items:center;gap:16px;margin-top:4px;flex-wrap:wrap}
.rv-review-actions .rv-btn{flex:1 1 220px;margin:0}
.rv-review .rv-review-actions + .rv-review-note{margin-top:14px}
/* WHILE GOOGLE IS OPEN BESIDE THE PAGE (owner correction). One quiet panel under the action:
   where Google went, and a way to bring its window forward - or, when the browser refused the
   window, a plain link. It is a note, not a second call to action, so it takes the ghost button
   and the inset surface, and it stays out of the way of the one thing still worth pressing -
   which is Google's own Publish, in the other window. */
.rv-review-window{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:14px;
  padding:12px 14px;border:1px solid var(--border-2,#23282A);border-radius:10px;
  background:var(--surface-2,#141817)}
.rv-review-window[hidden]{display:none}
.rv-review .rv-review-window .rv-review-window-msg{flex:1 1 220px;margin:0;font-size:13px;
  line-height:1.6;color:var(--text-hi)}
.rv-review .rv-review-window .rv-review-bring,
.rv-review .rv-review-window .rv-review-manual{flex:0 0 auto;min-height:44px;padding-inline:16px;
  font-size:13px}
.rv-review .rv-review-bring[hidden]{display:none}
@media (max-width:420px){
  .rv-review{padding:20px}
  .rv-review-actions .rv-btn{flex:1 1 100%}
}
