:root{
  --bg:#eef4fb;
  --bg2:#f7fafd;
  --navy:#25324a;
  --navy-soft:#5b6b85;
  --blue:#2f8dff;
  --blue-dark:#1c6fe0;
  --blue-light:#7fc0ff;
  --card:#ffffff;
  --line:#dbe6f2;
  --shadow: 0 10px 30px rgba(31,90,180,0.10);
  /* Раунд #105 — пиксельный шрифт для чисел аспектов в калькуляторе
     Centi-Vis (.cv-hud-value, см. @font-face и комментарий там же в
     разделе калькулятора). Переменная тут, в общих токенах, а не прямо
     в правиле — на случай, если этот же шрифт понадобится где-то ещё
     в майнкрафт-тематических местах сайта. */
  --font-minecraft: 'MinecraftRus', 'Courier New', monospace;
}
*{box-sizing:border-box;}
html{scroll-behavior:smooth; overflow-x:hidden;}
body{
  margin:0;
  min-height:100vh;
  background:
    radial-gradient(1200px 500px at 10% -10%, #ffffff 0%, transparent 60%),
    radial-gradient(1000px 600px at 100% 0%, #eaf3ff 0%, transparent 55%),
    linear-gradient(180deg,#f5f9fd 0%, #eef4fb 40%, #eaf1fa 100%);
  color:var(--navy);
  font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  overflow-x:hidden;
  position:relative;
}

/* decorative diagonal lines + cubes */
.deco-line{
  position:absolute;
  height:2px;
  background:linear-gradient(90deg, transparent, #8fbdf5, transparent);
  opacity:.55;
  transform-origin:left center;
}
.deco-band{
  position:absolute;
  background:linear-gradient(90deg, transparent 0%, rgba(178,208,250,.6) 45%, rgba(178,208,250,.35) 60%, transparent 100%);
  pointer-events:none;
  z-index:0;
}
.cube{
  position:absolute;
  will-change:transform;
  filter:drop-shadow(0 6px 10px rgba(30,100,220,.35));
}
.cube img{display:block;width:100%;height:100%;}
.cube.tiny{width:16px;height:16px;}
.cube.small{width:22px;height:22px;}
.cube.mid{width:30px;height:30px;}
.cube.big{width:40px;height:40px;}
.cube.xbig{width:52px;height:52px;}

.wrap{
  max-width:1752px;
  margin:0 auto;
  position:relative;
  padding:0 40px;
}

/* ---------- NAV ---------- */
header{
  position:relative;
  z-index:5;
  padding:26px 0 0;
}
.navbar{
  display:flex;
  align-items:center;
  justify-content:space-between;
}
.brand{
  display:flex;
  align-items:center;
  gap:8px;
  text-decoration:none;
  cursor:pointer;
}
.brand-icon{
  width:34px;height:34px;object-fit:contain;
}
.brand-text{
  line-height:1;
}
.brand-text .b1{
  font-weight:800;
  font-size:12px;
  letter-spacing:1px;
  color:var(--navy);
}
.brand-text .b2{
  font-size:8px;
  letter-spacing:2px;
  color:var(--blue-dark);
  font-weight:600;
}
nav ul{
  list-style:none;
  display:flex;
  gap:34px;
  margin:0;
  padding:0;
}
nav a{
  text-decoration:none;
  color:var(--navy-soft);
  font-size:13px;
  letter-spacing:1px;
  font-weight:600;
  padding-bottom:10px;
  position:relative;
  transition:color .2s;
  cursor:pointer;
  background:none;
  border:none;
}
nav a:hover{color:var(--blue-dark);}
nav a.active{color:var(--navy);}
nav a.active::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:0;
  height:2px;
  background:var(--blue);
  border-radius:2px;
}

/* "Помощь" nav item — not a section of its own, just a hover trigger for
   the dropdown (Правила / Политика / Команды). Styled by hand to match
   "nav a" above since it's a <span>, not an <a> (it doesn't navigate
   anywhere itself, only the three links inside the dropdown do). Shares
   .nav-item-dropdown below with the user menu (avatar/nickname) — same
   hover-dropdown mechanics, different trigger markup/styling. */
.nav-item-help{position:relative;}
.nav-link-help{
  display:inline-block;
  text-decoration:none;
  color:var(--navy-soft);
  font-size:13px;
  letter-spacing:1px;
  font-weight:600;
  padding-bottom:10px;
  cursor:pointer;
  transition:color .2s;
  position:relative;
}
.nav-item-help:hover .nav-link-help,
.nav-item-help:focus-within .nav-link-help{color:var(--blue-dark);}
/* Underline "Помощь" itself whenever Правила/Политика/Команды is the open
   section — set server-side on first load ($activeSection in
   header.blade.php) and client-side after an AJAX swap (setActiveLink()
   in app.js). Same treatment as "nav a.active::after" above, just written
   out by hand since .nav-link-help is a <span>, not an <a>. */
.nav-link-help.active{color:var(--navy);}
.nav-link-help.active::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:0;
  height:2px;
  background:var(--blue);
  border-radius:2px;
}
/* Clicking a dropdown link doesn't move the mouse (it's an AJAX swap, not
   a real navigation), so plain :hover would leave the dropdown sitting
   open over the new page. app.js adds this class on click and clears it
   on mouseleave — the !important is needed to win over :hover itself.
   Shared by both hover dropdowns (.nav-item-help and .nav-item-user). */
.nav-item-dropdown.force-closed .nav-dropdown{
  opacity:0 !important;
  visibility:hidden !important;
  pointer-events:none !important;
}
.nav-dropdown{
  list-style:none;
  margin:0;
  padding:10px;
  position:absolute;
  top:100%;
  left:50%;
  transform:translateX(-50%);
  min-width:230px;
  background:var(--card);
  border-radius:14px;
  box-shadow:0 20px 40px rgba(20,40,80,.18);
  display:flex;
  flex-direction:column;
  gap:2px;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  transition:opacity .18s ease;
  z-index:60;
}
/* Rightmost nav item (the user menu) — its dropdown must line up exactly
   under the avatar+nickname trigger, not wider or narrower and not
   off-center, so it's anchored with left:0/right:0 against the trigger's
   own box (.nav-item-user, position:relative below) instead of centering
   under it with a fixed min-width like .nav-dropdown's default above. */
.nav-dropdown.nav-dropdown-user{left:0; right:0; transform:none; min-width:0;}
.nav-item-dropdown:hover .nav-dropdown,
.nav-item-dropdown:focus-within .nav-dropdown{
  opacity:1;
  visibility:visible;
  pointer-events:auto;
}
.nav-dropdown a, .nav-dropdown button{
  display:flex;
  align-items:center;
  gap:10px;
  width:100%;
  padding:10px 14px;
  border:none;
  background:none;
  border-radius:9px;
  font:inherit;
  font-size:12.5px;
  letter-spacing:.3px;
  font-weight:600;
  color:var(--navy-soft);
  text-decoration:none;
  text-align:left;
  white-space:nowrap;
  cursor:pointer;
  transition:background .15s, color .15s;
}
.nav-dropdown a svg, .nav-dropdown button svg{
  width:16px;
  height:16px;
  flex:0 0 auto;
  color:#9aa7bc;
}
.nav-dropdown a:hover, .nav-dropdown a.active, .nav-dropdown button:hover{
  background:#eaf3ff;
  color:var(--blue-dark);
}
.nav-dropdown form{margin:0;}

.nav-buttons{
  display:flex;
  align-items:center;
  gap:12px;
}
/* User menu trigger (avatar + nickname + role) — see .nav-item-user in
   partials/header.blade.php. Square, not rounded — it's a crop of a
   blocky Minecraft skin, a hard edge suits it better than a rounded
   avatar would. Wrapped together with the nickname/role in one padded
   "chip" (.nav-user-trigger) so the whole thing reads as a real control,
   not a label sitting next to a button.
   Deliberately flat 2D here, NOT the skinview3d canvas the profile page
   uses (see session notes #40) — a nav link that's glanced at in passing
   should look straight back at you, not be turned a few degrees off-axis
   the way the profile's 3/4-angle 3D model is. This is just the skin
   texture's own front-facing head pixels, so there's no camera angle to
   get "wrong" in the first place. */
.skin-avatar{
  position:relative;
  width:48px;
  height:48px;
  overflow:hidden;
  flex:0 0 auto;
  background:#eaf3ff;
  box-shadow:0 0 0 1.5px var(--line) inset;
}
/* Two stacked crops of the same texture (--skin-url, set inline per-user in
   header.blade.php) — the overlay/"hat" layer (hair strands, etc. painted
   on the head's second UV layer) drawn on top of the base head layer
   underneath, exactly like real Minecraft renders composite it. Both
   layers are the same 64x64 atlas scaled 6x (so an 8px UV region fills
   this 48px box) and shifted via background-position to the head's front
   UV coordinates — base at (8,8), overlay at (40,8), per the skin format
   (see session notes #37 for the full 12-region map this project has used
   elsewhere). For a skin with nothing painted on the overlay (default
   Steve, most skins) that layer is fully transparent and simply lets the
   base layer show through untouched. */
.skin-avatar-face{
  position:absolute;
  inset:0;
  background-image:var(--skin-url), var(--skin-url);
  background-repeat:no-repeat, no-repeat;
  /* Fixed on purpose — NOT scaled by skin resolution. background-size in
     px always fits the WHOLE source bitmap into that box regardless of
     its native pixel dimensions (a 1024x1024 HD skin gets scaled DOWN to
     384x384 here, a 64x64 one scaled UP to it) — so -48px/-240px are
     already fixed fractions of the whole image (12.5%/62.5%), correct
     for any resolution as long as the texture is square. Do not multiply
     these by a scale factor — that was tried and is wrong: it makes the
     crop zoom into a tiny corner of an HD skin instead of the head (see
     session notes — the HD-skin avatar bug this caused). The only case
     these values need help is a non-square (64x32 legacy-shaped) source,
     and that's fixed upstream by ProfileController::upgradeLegacySkin()
     squaring the file up at upload time, not here. */
  background-size:384px 384px, 384px 384px;
  background-position:-240px -48px, -48px -48px;
  image-rendering:pixelated;
  image-rendering:-moz-crisp-edges;
  image-rendering:crisp-edges;
}
/* min-width guards against short usernames (3-4 chars — the avatar+text
   chip above shrinks to fit its content, and .nav-dropdown-user below is
   pinned to that same width via left:0/right:0, see the comment on it
   above). Below ~120px the dropdown gets narrower than "Профиль"/"Выйти"
   themselves need (icon + nowrap text + padding), so the text pokes past
   the dropdown's own rounded edge instead of sitting inside it — this
   keeps the chip (and so the dropdown) at a safe minimum regardless of
   nickname length. Set here rather than on .nav-buttons itself: that's a
   plain flex row and its children size to content by default, so a
   min-width there wouldn't have propagated down to this item at all. */
.nav-item-user{position:relative; min-width:150px;}
.nav-user-trigger{
  display:flex;
  align-items:center;
  gap:12px;
  padding:6px 16px 6px 6px;
  border-radius:12px;
  text-decoration:none;
  cursor:pointer;
  transition:background .18s;
}
.nav-item-dropdown:hover .nav-user-trigger,
.nav-item-dropdown:focus-within .nav-user-trigger{background:#eaf3ff;}
.nav-user-text{
  display:flex;
  flex-direction:column;
  gap:2px;
  line-height:1.2;
}
.nav-item-user:hover .nav-username,
.nav-item-user:focus-within .nav-username{color:var(--blue-dark);}
.nav-username{
  font-weight:800;
  font-size:15px;
  letter-spacing:.3px;
  color:var(--navy);
  max-width:170px;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  transition:color .2s;
}
.nav-role{
  font-size:11.5px;
  letter-spacing:.3px;
  color:#8a97ad;
}
.btn-login{
  display:flex;
  align-items:center;
  gap:8px;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  color:#fff;
  border:none;
  padding:12px 22px;
  border-radius:10px;
  font-weight:700;
  font-size:13px;
  letter-spacing:1px;
  cursor:pointer;
  box-shadow:0 10px 22px rgba(28,111,224,.35);
  transition:background .2s, box-shadow .2s;
}
.btn-login:hover{
  background:linear-gradient(135deg,#5aacff,#2f7ae5);
  box-shadow:0 12px 26px rgba(28,111,224,.45);
}
.btn-login svg{width:16px;height:16px;}
.btn-register{
  background:transparent;
  color:var(--blue-dark);
  border:1.5px solid #bcd6f7;
  padding:11px 20px;
  border-radius:10px;
  font-weight:700;
  font-size:13px;
  letter-spacing:1px;
  cursor:pointer;
  transition:background .2s, border-color .2s;
}
.btn-register:hover{background:#eaf3ff; border-color:var(--blue);}

/* ---------- HERO ---------- */
.hero{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:30px 0 10px;
  min-height:480px;
  perspective:1200px;
}
.island{
  width:41%;
  max-width:680px;
  position:relative;
  z-index:2;
  filter:drop-shadow(0 20px 30px rgba(40,90,170,.18));
  will-change:transform;
}
.island img{width:100%;display:block;}
.island.left{margin-left:-60px;}
.island.right{margin-right:-60px;}

.hero-center{
  flex:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  padding:0 20px;
  position:relative;
  z-index:3;
}
.logo-icon-hero{
  width:150px;
  height:auto;
  margin-bottom:6px;
  position:relative;
}
.hero-title{
  font-size:52px;
  font-weight:800;
  letter-spacing:3px;
  margin:0;
  line-height:1;
  background:radial-gradient(ellipse 140% 240% at 50% -60%, #ffffff 0%, #d3e3f7 20%, #7288a8 45%, var(--navy) 72%);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
}
.hero-sub{
  font-size:24px;
  font-weight:700;
  letter-spacing:10px;
  background:linear-gradient(90deg,#3f9bff,#1c6fe0);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  margin:2px 0 10px;
}
.hero-tag{
  font-size:12px;
  letter-spacing:4px;
  color:#b7c0d1;
  font-weight:400;
  margin-bottom:28px;
}

.online-card{
  display:flex;
  align-items:center;
  gap:14px;
  background:var(--card);
  border-radius:16px;
  padding:16px 26px;
  box-shadow:var(--shadow);
  margin-bottom:22px;
}
.online-card .icon{
  width:38px;height:38px;
  display:flex;align-items:center;justify-content:center;
  color:var(--blue-dark);
}
.online-card .icon svg{width:26px;height:26px;}
/* min-width keeps the card's proportions steady regardless of how wide
   the live number is — "142 / 1000" and "0 / 40" shouldn't make the
   whole card visibly resize/re-center every poll (session notes #54). */
.online-text{text-align:left; min-width:92px;}
.online-text .label{
  font-size:10px;
  letter-spacing:1.5px;
  color:#93a1b8;
  font-weight:500;
}
.online-text .value{
  font-size:21px;
  font-weight:700;
  color:var(--navy);
  font-variant-numeric:tabular-nums;
  letter-spacing:.2px;
}
.dot{
  width:9px;height:9px;
  border-radius:50%;
  background:#39d97a;
  box-shadow:0 0 0 4px rgba(57,217,122,.18);
  margin-left:4px;
  transition:background .25s ease, box-shadow .25s ease;
}
/* Subtle "alive" pulse — only while genuinely online (not loading/offline). */
.dot:not(.is-loading):not(.is-offline){
  animation:online-dot-pulse 2.4s ease-in-out infinite;
}
@keyframes online-dot-pulse{
  0%, 100%{ box-shadow:0 0 0 4px rgba(57,217,122,.18); }
  50%{ box-shadow:0 0 0 7px rgba(57,217,122,.05); }
}
/* Before the first /server-status response lands — see hero.blade.php. */
.dot.is-loading{
  background:#c7cedc;
  box-shadow:0 0 0 4px rgba(199,206,220,.18);
}
.dot.is-offline{
  background:#ff4d4f;
  box-shadow:0 0 0 4px rgba(255,77,79,.18);
}

.btn-connect{
  display:flex;
  align-items:center;
  gap:10px;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  color:#fff;
  border:none;
  padding:16px 30px;
  border-radius:12px;
  font-weight:700;
  font-size:14px;
  letter-spacing:1.5px;
  cursor:pointer;
  box-shadow:0 14px 26px rgba(28,111,224,.4);
  transition:transform .25s cubic-bezier(.34,1.56,.64,1), box-shadow .25s ease;
}
.btn-connect:hover{
  transform:translateY(-4px) scale(1.045);
  box-shadow:0 20px 36px rgba(28,111,224,.5);
}
.btn-connect:active{
  transform:translateY(-1px) scale(1.01);
}
.btn-connect:disabled{
  opacity:.45;
  cursor:not-allowed;
  box-shadow:none;
}
.btn-connect:disabled:hover{
  transform:none;
  box-shadow:none;
}
.btn-connect:hover .arrow{
  transform:translateX(4px);
}
.btn-connect svg{width:18px;height:18px;}
.btn-connect .arrow{margin-left:6px; transition:transform .25s ease;}

/* ---------- MODS ---------- */
.mods{
  position:absolute;
  right:36px;
  top:374px;
  display:flex;
  gap:26px;
  padding:18px 26px;
  background:rgba(255,255,255,.5);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border-radius:18px;
  box-shadow:0 10px 26px rgba(31,90,180,.08);
  z-index:4;
}
.mods.left{
  right:auto;
  left:36px;
}
.mod-item{
  text-align:center;
  width:88px;
}
.mod-icon-frame{
  width:54px;height:54px;
  margin:0 auto 12px;
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
}
/* Colour comes from CSS custom properties set inline per mod (sampled from
   each mod's own icon artwork — see the $modColors array in hero.blade.php)
   rather than a fixed set of named classes, so a diamond's colour always
   matches the icon sitting inside it. Falls back to the original blue if a
   mod is ever added without a computed colour. */
.mod-icon-frame::before{
  content:"";
  position:absolute;
  inset:0;
  background:linear-gradient(135deg, var(--frame-bg1, #eef5ff), var(--frame-bg2, #d7e8fb));
  border:1.5px solid var(--frame-border, #bcd6f7);
  border-radius:9px;
  transform:rotate(45deg);
  box-shadow:0 8px 16px var(--frame-shadow, rgba(31,90,180,.16)), inset 0 1px 0 rgba(255,255,255,.55);
  opacity:.85;
}
/* Sized/positioned by frame (not by .mod-item) so any icon placed inside a
   .mod-icon-frame — the hero showcase, a family header, a plain list row —
   sits centred and on top of the diamond, scaled to that frame's size. */
.mod-icon-frame img{
  width:56%;height:56%;
  object-fit:contain;
  position:relative;
  z-index:1;
  filter:drop-shadow(0 3px 6px rgba(30,60,120,.22));
}
.mod-icon-frame img.pixel-icon{
  image-rendering:pixelated;
  image-rendering:-moz-crisp-edges;
  image-rendering:crisp-edges;
}
.mod-item .name{
  font-size:12px;
  font-weight:800;
  color:var(--navy);
}
.mod-item .desc{
  font-size:9px;
  letter-spacing:.5px;
  color:#96a3b8;
  font-weight:400;
  text-transform:uppercase;
  line-height:1.3;
  margin-top:2px;
}

/* ---------- DYNAMIC CONTENT (AJAX-swapped block) ---------- */
#dynamic-content{
  position:relative;
  min-height:200px;
}
#dynamic-content.is-loading{
  opacity:.35;
  pointer-events:none;
  transition:opacity .15s ease;
}
#dynamic-content.is-ready{
  animation:section-fade-in .35s ease both;
}
@keyframes section-fade-in{
  from{opacity:0; transform:translateY(8px);}
  to{opacity:1; transform:translateY(0);}
}

/* ---------- NEWS ---------- */
.news-heading{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:18px;
  margin:70px 0 34px;
  position:relative;
  z-index:3;
}
.news-heading .line{
  width:70px;height:2px;
  background:linear-gradient(90deg,transparent,#b9cde8);
}
.news-heading .line.r{background:linear-gradient(90deg,#b9cde8,transparent);}
.news-heading h2{
  margin:0;
  font-size:26px;
  letter-spacing:5px;
  font-weight:800;
  color:var(--navy);
}

.news-grid{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:28px;
  max-width:1120px;
  margin:0 auto;
  padding-bottom:16px;
  position:relative;
  z-index:3;
}
.news-card{
  /* Now an <a> (see sections/home.blade.php — cards are clickable,
     session notes #45), not an <article>: display:flex below already
     overrides the anchor's default inline, these two just undo the
     browser's default link styling so it still reads as a card, not
     a link. */
  text-decoration:none;
  color:inherit;
  background:var(--card);
  border-radius:14px;
  overflow:hidden;
  box-shadow:var(--shadow);
  display:flex;
  flex-direction:column;
  transition:transform .25s ease, box-shadow .25s ease;
}
.news-card:hover{
  transform:translateY(-4px) scale(1.025);
  box-shadow:0 18px 36px rgba(31,90,180,.18);
}
.news-thumb{
  position:relative;
  height:104px;
}
.news-thumb img{
  width:100%;height:100%;object-fit:cover;display:block;
}
.badge{
  position:absolute;
  top:14px;left:14px;
  background:#2f8dff;
  color:#fff;
  font-size:10px;
  font-weight:400;
  letter-spacing:1px;
  padding:6px 12px;
  border-radius:6px;
}
.news-body{
  padding:20px 22px 22px;
  flex:1;
  display:flex;
  flex-direction:column;
}
.news-body h3{
  margin:0 0 8px;
  font-size:16px;
  font-weight:800;
  color:var(--navy);
}
.news-body p{
  margin:0;
  font-size:13px;
  line-height:1.6;
  color:#8493aa;
  flex:1;
}
.news-foot{
  display:flex;
  align-items:center;
  justify-content:space-between;
  margin-top:18px;
  padding-top:14px;
  border-top:1px solid var(--line);
}
/* Groups .news-date + .news-views together on the left of .news-foot/
   .news-list-foot, so .news-arrow (card only) still gets pushed all the
   way right by justify-content:space-between on the parent. */
.news-meta-group{
  display:flex;
  align-items:center;
  gap:16px;
}
.news-date{
  display:flex;
  align-items:center;
  gap:7px;
  font-size:12px;
  color:#93a1b8;
  font-weight:600;
}
.news-date svg{width:14px;height:14px;}
.news-views{
  display:flex;
  align-items:center;
  gap:7px;
  font-size:12px;
  color:#93a1b8;
  font-weight:600;
}
.news-views svg{width:14px;height:14px;}
.news-arrow{
  width:26px;height:26px;
  display:flex;align-items:center;justify-content:center;
  color:var(--blue-dark);
}
.news-arrow svg{width:18px;height:18px;}

.news-more{
  display:flex;
  justify-content:center;
  margin-top:6px;
}
.btn-all-news{
  background:transparent;
  color:var(--blue-dark);
  border:1.5px solid #bcd6f7;
  padding:13px 30px;
  border-radius:11px;
  font-weight:700;
  font-size:13px;
  letter-spacing:1px;
  cursor:pointer;
  transition:background .2s, border-color .2s, transform .2s;
}
.btn-all-news:hover{background:#eaf3ff; border-color:var(--blue); transform:translateY(-2px);}
/* Same button reused for "Загрузить ещё" on the news list (sections/
   news.blade.php + public/js/app.js) — disabled state while the AJAX
   request for the next page is in flight. */
.btn-all-news:disabled{opacity:.55; cursor:default; transform:none;}

/* ---------- NEWS LIST (страница «Новости») ---------- */
/* The home page shows the latest 3 news as a card grid (.news-grid above);
   the full "Новости" page shows the whole history as a single-column list
   of rows instead — reuses .badge/.news-date/.news-arrow from the card so
   the two views still read as the same design language. */
.news-list{
  max-width:900px;
  margin:0 auto 20px;
  background:var(--card);
  border-radius:18px;
  box-shadow:var(--shadow);
  overflow:hidden;
  position:relative;
  z-index:3;
}
/* Shown instead of .news-list when the `news` table has nothing published
   (see sections/news.blade.php) — same card treatment as .news-list itself
   so an empty state doesn't look like a layout gap. */
.news-empty{
  max-width:900px;
  margin:0 auto 20px;
  padding:40px 26px;
  background:var(--card);
  border-radius:18px;
  box-shadow:var(--shadow);
  text-align:center;
  color:var(--navy-soft);
  font-size:14px;
  position:relative;
  z-index:3;
}
.news-list-row{
  /* Now an <a> too (see sections/news.blade.php, session notes #45) —
     same reasoning as .news-card above. */
  text-decoration:none;
  color:inherit;
  display:flex;
  gap:22px;
  padding:22px 26px;
  border-bottom:1px solid var(--line);
  transition:background .15s ease;
}
.news-list-row:last-child{border-bottom:none;}
.news-list-row:hover{background:#f6faff;}
.news-list-thumb{
  position:relative;
  width:150px;
  height:100px;
  border-radius:12px;
  overflow:hidden;
  flex-shrink:0;
}
.news-list-thumb img{
  width:100%;height:100%;
  object-fit:cover;
  display:block;
}
.news-list-thumb .badge{
  top:8px; left:8px;
  padding:5px 10px;
  font-size:9px;
}
.news-list-body{
  flex:1;
  min-width:0;
  display:flex;
  flex-direction:column;
}
.news-list-body h3{
  margin:0 0 8px;
  font-size:16px;
  font-weight:800;
  color:var(--navy);
}
.news-list-body p{
  margin:0;
  font-size:13px;
  line-height:1.6;
  color:#8493aa;
  flex:1;
}
.news-list-foot{
  display:flex;
  align-items:center;
  justify-content:space-between;
  margin-top:14px;
}

/* ---------- CONTACTS PAGE (страница «Контакты») ---------- */
/* The only real contact channel is Discord. Two plain rectangular panels
   (light pitch + dark Discord-widget card) with no hard seam at all — the
   site's own floating crystal cubes are scattered loosely across the whole
   hero, straddling both the light and dark halves in different spots, at
   different sizes and small random tilts, for a deliberately careless
   "clutter" feel instead of a neat line down the middle. */
.contacts-hero{
  position:relative;
  display:flex;
  align-items:stretch;
  min-height:clamp(420px, 58vh, 560px);
  max-width:1120px;
  margin:0 auto 20px;
  border-radius:26px;
  overflow:hidden;
  box-shadow:var(--shadow);
  z-index:3;
  background:linear-gradient(160deg, #ffffff 0%, #eef4fc 100%);
}
.contacts-cubes{
  position:absolute;
  inset:0;
  z-index:3;
  pointer-events:none;
}
.contacts-cubes .cube{opacity:.92;}
.contacts-hero-light{
  position:relative;
  z-index:2;
  flex:1 1 46%;
  display:flex;
  flex-direction:column;
  justify-content:center;
  padding:50px 40px 50px 54px;
}
.contacts-kicker{
  margin-bottom:14px;
  font-size:12px;
  font-weight:800;
  letter-spacing:2.5px;
  text-transform:uppercase;
  color:var(--blue-dark);
}
.contacts-hero-light h2{
  margin:0 0 16px;
  font-size:29px;
  font-weight:800;
  letter-spacing:.3px;
  color:var(--navy);
}
.contacts-hero-light p{
  margin:0 0 26px;
  font-size:14.5px;
  line-height:1.75;
  color:var(--navy-soft);
  max-width:320px;
}
.discord-invite-plain{
  display:inline-flex;
  align-items:center;
  align-self:flex-start;
  gap:9px;
  padding:11px 18px;
  border-radius:11px;
  background:#eaf3ff;
  border:1px solid #d7e7fb;
  color:var(--blue-dark);
  font-weight:700;
  font-size:13px;
  letter-spacing:.3px;
}
.contacts-hero-dark{
  position:relative;
  z-index:1;
  flex:1 1 54%;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:36px 44px;
  background:
    radial-gradient(480px 320px at 75% 0%, rgba(127,192,255,.35) 0%, transparent 65%),
    linear-gradient(165deg, #32456c 0%, #22304d 55%, #11182a 100%);
}
.discord-widget{
  position:relative;
  z-index:1;
  width:100%;
  max-width:280px;
  background:rgba(15,20,38,.55);
  border:1px solid rgba(255,255,255,.1);
  border-radius:16px;
  overflow:hidden;
  backdrop-filter:blur(6px);
  box-shadow:0 20px 44px rgba(5,8,24,.45);
}
.discord-widget-head{
  display:flex;
  align-items:center;
  gap:10px;
  padding:14px 16px;
  background:linear-gradient(135deg,#5865f2,#4752c4);
}
.discord-widget-head svg{width:20px; height:20px; fill:#fff; flex-shrink:0;}
.discord-widget-head span{
  font-size:13px;
  font-weight:800;
  letter-spacing:.4px;
  color:#fff;
}
.discord-widget-dot{
  margin-left:auto;
  width:8px; height:8px;
  border-radius:50%;
  background:#3ba55c;
  box-shadow:0 0 0 3px rgba(59,165,92,.25);
}
.discord-widget-body{
  padding:22px 18px 24px;
  text-align:center;
}
.discord-widget-body p{
  margin:0 0 18px;
  font-size:12.5px;
  line-height:1.6;
  color:rgba(255,255,255,.72);
}
.btn-discord-join{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:10px;
  width:100%;
  background:#fff;
  color:#4752c4;
  border:none;
  padding:13px 18px;
  border-radius:11px;
  font-weight:800;
  font-size:13px;
  letter-spacing:.2px;
  text-decoration:none;
  box-shadow:0 12px 26px rgba(5,8,24,.35);
  transition:transform .25s cubic-bezier(.34,1.56,.64,1), box-shadow .25s ease;
}
.btn-discord-join:hover{
  transform:translateY(-3px) scale(1.02);
  box-shadow:0 16px 32px rgba(5,8,24,.42);
}
.btn-discord-join:active{transform:translateY(-1px) scale(1.005);}
.btn-discord-join svg{width:17px; height:17px; fill:#5865f2; flex-shrink:0;}
.btn-discord-join .arrow{width:15px; height:15px; margin-left:1px; transition:transform .25s ease;}
.btn-discord-join:hover .arrow{transform:translateX(3px);}

/* ---------- TOP PLAYERS ---------- */
.leaders{
  max-width:820px;
  margin:0 auto;
  padding:0 0 20px;
  position:relative;
  z-index:3;
}
.leaders-card{
  background:var(--card);
  border-radius:16px;
  box-shadow:var(--shadow);
  overflow:hidden;
}
.leader-row{
  display:flex;
  align-items:center;
  gap:16px;
  padding:16px 26px;
  border-bottom:1px solid var(--line);
  transition:background .2s;
}
.leader-row:last-child{border-bottom:none;}
.leader-row:hover{background:#f6faff;}
.leader-rank{
  width:30px;
  font-size:14px;
  font-weight:800;
  color:#a7b2c4;
  text-align:center;
  flex-shrink:0;
}
.leader-row.top .leader-rank{
  width:30px;height:30px;
  border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  color:#fff;
  font-size:13px;
}
.leader-row.rank-1 .leader-rank{background:linear-gradient(135deg,#ffd873,#e8a812);}
.leader-row.rank-2 .leader-rank{background:linear-gradient(135deg,#dbe4ee,#a9b6c8);}
.leader-row.rank-3 .leader-rank{background:linear-gradient(135deg,#e5b083,#b9773f);}
.leader-avatar{
  position:relative;
  width:38px;height:38px;
  border-radius:10px;
  flex-shrink:0;
  overflow:hidden;
  /* Neutral fallback fill behind the skin crop — same idea as
     .roster-avatar/.backend-avatar, visible for a split second before
     the texture loads rather than a flash of pure white. */
  background:#e1e7f0;
}
/* Real skin face crop — every row here is a genuine site account
   (player_playtime_monthly.user_id is a real FK, not a nickname match
   like clan roster), so unlike .roster-avatar-face there's no letter/
   gradient fallback needed underneath, always a real texture. Same
   two-layer technique as .skin-avatar-face, recalculated for this 38px
   box: scale = 38/8 = 4.75, bg-size = 64*4.75 = 304px, base head
   UV(8,8)*4.75 = -38px -38px, overlay UV(40,8)*4.75 = -190px -38px. */
.leader-avatar-face{
  position:absolute;
  inset:0;
  background-image:var(--skin-url), var(--skin-url);
  background-repeat:no-repeat, no-repeat;
  background-size:304px 304px, 304px 304px;
  background-position:-190px -38px, -38px -38px;
  image-rendering:pixelated;
  image-rendering:-moz-crisp-edges;
  image-rendering:crisp-edges;
}
/* Small "в игре сейчас" indicator, bottom-right corner of the avatar —
   derived from User::is_online_in_game (last_game_login_at vs
   last_game_logout_at, written by the ArcanePlaytime plugin), not the
   website's own last_seen_at online status. The card-colored ring
   (box-shadow, not border — a border would shrink the dot itself)
   is what makes it read as "cut into" the avatar rather than just a
   dot floating on top of it. */
.leader-status-dot{
  position:absolute;
  right:-1px; bottom:-1px;
  width:11px; height:11px;
  border-radius:50%;
  background:#2f9c66;
  box-shadow:0 0 0 2px var(--card);
}
.leader-info{flex:1; min-width:0;}
.leader-name{
  font-size:14px;
  font-weight:700;
  color:var(--navy);
}
.leader-label{
  font-size:10px;
  letter-spacing:.5px;
  color:#9aa5b8;
  text-transform:uppercase;
  margin-top:1px;
}
.leader-time{
  font-size:14px;
  font-weight:700;
  color:var(--blue-dark);
  flex-shrink:0;
}

@media (max-width: 560px){
  .leader-row{padding:14px 16px; gap:10px;}
  .leader-name{font-size:13px;}
  .leader-time{font-size:12px;}
}

/* ---------- INFO SECTIONS (О сервере / О клане / Правила / Карта / Контакты) ---------- */
.info-card{
  max-width:920px;
  margin:0 auto 20px;
  background:var(--card);
  border-radius:18px;
  box-shadow:var(--shadow);
  padding:36px 40px 40px;
  position:relative;
  z-index:3;
}
.info-lead{
  margin:0 0 24px;
  font-size:15px;
  line-height:1.7;
  color:#5b6b85;
}
.info-lead code, .info-note code{
  background:#eef4fb;
  color:var(--blue-dark);
  border-radius:5px;
  padding:2px 6px;
  font-size:13px;
}
.info-list{
  margin:0 0 20px;
  padding:0;
  list-style:none;
  display:flex;
  flex-direction:column;
  gap:10px;
}
.info-list li{
  position:relative;
  padding-left:26px;
  font-size:13.5px;
  line-height:1.6;
  color:#5b6b85;
}
.info-list li::before{
  content:"";
  position:absolute;
  left:0; top:7px;
  width:9px;height:9px;
  border-radius:50%;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
}
.info-note{
  margin:0;
  font-size:13px;
  color:#93a1b8;
}
.info-note a, .info-lead a{
  color:var(--blue-dark);
  font-weight:600;
  text-decoration:none;
}
.info-note a:hover, .info-lead a:hover{text-decoration:underline;}
.info-rules{
  margin:0;
  padding-left:22px;
  display:flex;
  flex-direction:column;
  gap:12px;
}
.info-rules li{
  font-size:13.5px;
  line-height:1.6;
  color:#5b6b85;
  padding-left:6px;
}
.info-rules li::marker{
  font-weight:800;
  color:var(--blue-dark);
}

/* ---------- COMMAND LIST (Команды сервера) ---------- */
.cmd-list{
  display:flex;
  flex-direction:column;
  gap:8px;
}
.cmd-row{
  display:flex;
  align-items:baseline;
  flex-wrap:wrap;
  gap:6px 14px;
  padding:12px 16px;
  border-radius:10px;
  background:#f6faff;
  border:1px solid var(--line);
}
.cmd-code{
  flex:0 0 auto;
  font-family:'Courier New', monospace;
  font-size:12.5px;
  font-weight:700;
  color:var(--blue-dark);
  background:#eaf3ff;
  padding:4px 10px;
  border-radius:6px;
  white-space:nowrap;
}
.cmd-desc{
  font-size:13px;
  line-height:1.6;
  color:#5b6b85;
}

/* ---------- SKIN RENDERER (профиль + header avatar) ----------
   A real 3D model (skinview3d, three.js-based — see app.js) instead of the
   flat CSS crops this used to be. The flat "paper doll" approach (front/
   back crops swapped on hover) couldn't show hair drawn on the head's
   side textures or a genuinely inflated overlay cube, and kept coming up
   short against real Minecraft renders — see session notes #36-38 for the
   escalating patches, and #39 for the actual switch. skinview3d owns the
   canvas entirely (rotation, lighting, the model itself); the rules below
   are just its container framing. */
.skin-viewer-canvas{
  display:block;
  margin:0 auto;
  cursor:grab;
}
.skin-viewer-canvas:active{cursor:grabbing;}

/* Podium behind the skin figure — a soft gradient panel plus a drawn
   ground-shadow ellipse, purely decorative (aria-hidden'd where used). */
.skin-stage{
  position:relative;
  width:216px;
  margin:0 auto 16px;
  padding:26px 0 24px;
  border-radius:20px;
  background:
    radial-gradient(ellipse at 50% 90%, rgba(63,155,255,.24), transparent 65%),
    linear-gradient(180deg, #eef5ff 0%, #f8fbff 100%);
  border:1px solid var(--line);
  overflow:hidden;
}
.skin-stage::after{
  content:"";
  position:absolute;
  left:50%; bottom:22px;
  width:118px; height:16px;
  transform:translateX(-50%);
  border-radius:50%;
  background:radial-gradient(ellipse, rgba(40,90,170,.24), transparent 72%);
  z-index:0;
}
.skin-stage .skin-viewer-canvas{position:relative; z-index:1;}

.btn-skin-change{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  width:100%;
  padding:10px 16px;
  border:1.5px solid var(--blue);
  border-radius:10px;
  background:#fff;
  color:var(--blue-dark);
  font-size:12.5px;
  font-weight:700;
  letter-spacing:.3px;
  cursor:pointer;
  transition:background .2s, color .2s;
}
.btn-skin-change:hover{background:var(--blue); color:#fff;}
.btn-skin-change svg{width:15px; height:15px; flex:0 0 auto;}
.btn-skin-change input[type="file"]{
  position:absolute;
  width:1px; height:1px;
  padding:0; margin:-1px;
  overflow:hidden;
  clip:rect(0,0,0,0);
  white-space:nowrap;
  border:0;
}
.btn-skin-change.is-loading{opacity:.6; pointer-events:none;}
/* Same width as .skin-stage above it — without this, a long field-error
   (see the "Скин должен быть..." message in ProfileController::updateSkin())
   has nothing to wrap against and stretches the column wider than the
   skin preview sitting right above it. */
.skin-upload-form{margin-top:0; max-width:216px;}
.skin-upload-form .field-error{margin-top:6px;}
.skin-upload-form .modal-msg{margin-top:6px;}
/* .modal-msg (в отличие от .field-error чуть выше — см. его :empty-
   правило) держит 18px высоты даже пустым — так задумано в модалках
   входа/регистрации, где под неё зарезервировано место заранее. Тут
   (плащ ближе к кнопке скина, см. .skin-upload-form + .skin-upload-form
   ниже) это давало лишние ~24px пустоты между формами — схлопываем,
   только пока форма реально пустая (текст появится — снова займёт место
   как обычно). */
.skin-upload-form .modal-msg:empty{min-height:0; margin-top:0;}
/* Кнопка загрузки плаща — та же .skin-upload-form сразу следом за формой
   скина (profile.blade.php, session notes #76/#77) — без этого правила
   они слипаются вплотную друг к другу; значение небольшое специально —
   плащ должен сидеть заметно ближе к скину, чем произвольный блок ниже. */
.skin-upload-form + .skin-upload-form{margin-top:6px;}

/* ---------- PROFILE PAGE ----------
   Deliberately not .info-card like the other sections — this is the
   player's own page, so it borrows the hero's premium feel instead
   (gradient text, a soft glow background, a couple of the same floating
   cubes as .hero, see partials/hero-decor.blade.php) rather than sitting
   in a plain flat card like Правила/Контакты/etc. */
.profile-hero{
  position:relative;
  overflow:hidden;
  max-width:960px;
  margin:0 auto 20px;
  padding:48px 52px;
  border-radius:26px;
  background:
    radial-gradient(circle at 12% 18%, rgba(120,190,255,.22), transparent 55%),
    radial-gradient(circle at 90% 88%, rgba(63,155,255,.16), transparent 50%),
    var(--card);
  box-shadow:0 24px 60px rgba(31,90,180,.16);
  display:flex;
  align-items:center;
  gap:52px;
}
.profile-hero-decor{position:absolute; inset:0; z-index:0; pointer-events:none;}
/* cube-4.png (41x46) / cube-5.png (61x68) — the two highest-resolution cube
   assets in public/images, shown near their native size so they stay crisp
   instead of the blurry 3-4x upscale the smaller cubes needed at this size. */
.profile-cube{position:absolute; filter:drop-shadow(0 10px 18px rgba(40,90,170,.18)); opacity:.85;}
.profile-cube.c1{width:61px; top:8%;  right:8%;  transform:rotate(-14deg);}
.profile-cube.c2{width:41px; top:62%; right:23%; transform:rotate(18deg);}

.profile-skin-col{position:relative; z-index:1; flex:0 0 auto;}
.profile-info-col{position:relative; z-index:1; flex:1; min-width:0;}

.profile-role-badge{
  display:inline-block;
  background:#eaf3ff;
  color:var(--blue-dark);
  font-size:11px;
  font-weight:700;
  letter-spacing:1.5px;
  text-transform:uppercase;
  padding:5px 14px;
  border-radius:20px;
  margin-bottom:12px;
}
.profile-username{
  margin:0 0 26px;
  font-size:36px;
  font-weight:800;
  letter-spacing:.5px;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
}
.profile-stats{
  display:flex;
  flex-direction:column;
  gap:10px;
  margin-bottom:22px;
}
.profile-stat{
  display:flex;
  align-items:center;
  gap:14px;
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:12px;
  padding:11px 16px;
}
.profile-stat-icon{
  width:36px;
  height:36px;
  flex:0 0 auto;
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:10px;
  background:#eaf3ff;
  color:var(--blue-dark);
}
.profile-stat-icon svg{width:17px; height:17px;}
.profile-stat-text{display:flex; flex-direction:column; gap:2px;}
.profile-stat-label{font-size:12px; color:#5b6b85;}
.profile-stat-value{font-size:13.5px; font-weight:700; color:var(--navy);}

/* Основное / Плащ / Статус / Префикс — own namespace (.profile-tab*), not
   a generalized version of .clan-tab* above, even though the visual style
   (bottom-border nav + sliding underline) is deliberately the same. Kept
   separate so nothing here can affect the already-shipped clan tabs. */
.profile-tabs-nav{
  display:flex;
  flex-wrap:wrap;
  gap:0 22px;
  margin-bottom:20px;
  border-bottom:1.5px solid var(--line);
}
.profile-tab-btn{
  background:none;
  border:none;
  font:inherit;
  font-weight:700;
  font-size:13px;
  letter-spacing:.4px;
  color:var(--navy-soft);
  padding:9px 2px 12px;
  margin-bottom:-1.5px;
  cursor:pointer;
  position:relative;
  transition:color .2s;
}
.profile-tab-btn:hover{color:var(--blue-dark);}
.profile-tab-btn.active{color:var(--navy);}
.profile-tab-btn.active::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:-1.5px;
  height:2px;
  background:var(--blue);
  border-radius:2px;
}
.profile-placeholder{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  padding:34px 20px;
  text-align:center;
  background:#f6faff;
  border:1px dashed var(--line);
  border-radius:12px;
  color:#8a97ad;
}
.profile-placeholder svg{width:26px; height:26px; opacity:.7;}
.profile-placeholder-text{font-size:13px; font-weight:600;}

/* ---------- PROFILE — PREFIX TAB (session notes #84) ----------
   Свой игровой префикс ника, form на .prefix-form внутри
   [data-profile-panel="prefix"] (profile.blade.php) — тот же общий язык
   форм, что у .modal-field выше (тот же input-стиль), плюс собственное
   превью-"плашка чата" и свотчи 16 стандартных цветов Minecraft (см.
   App\Support\MinecraftColors). JS — initPrefixTab() в app.js. */
.prefix-preview{
  display:flex;
  flex-wrap:wrap;
  align-items:baseline;
  gap:5px;
  padding:14px 18px;
  border-radius:12px;
  background:#1b2334;
  font-family:'Courier New', Consolas, monospace;
  font-size:13.5px;
  font-weight:700;
  margin-bottom:16px;
  word-break:break-word;
}
.prefix-preview-rest{color:#8a97ad; font-weight:400;}

.prefix-field-label{
  display:block;
  font-size:12px;
  font-weight:700;
  letter-spacing:.3px;
  color:#5b6b85;
  text-transform:uppercase;
  margin-bottom:6px;
}
.prefix-text-input{
  width:100%;
  font:inherit;
  font-weight:400;
  padding:11px 14px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:#f8fafd;
  color:var(--navy);
  transition:border-color .2s, background .2s;
}
.prefix-text-input:focus{outline:none; border-color:var(--blue); background:#fff;}
.prefix-text-input::placeholder{color:#b7c3d6; opacity:1;}

.prefix-color-group{margin-top:16px;}
.prefix-color-group-label{
  display:block;
  font-size:12px;
  font-weight:700;
  letter-spacing:.3px;
  color:#5b6b85;
  text-transform:uppercase;
  margin-bottom:8px;
}
.prefix-swatches{display:flex; flex-wrap:wrap; gap:8px;}
.prefix-swatch{
  width:28px; height:28px;
  border-radius:8px;
  border:2px solid transparent;
  box-shadow:0 0 0 1px var(--line) inset;
  cursor:pointer;
  padding:0;
  transition:transform .15s, box-shadow .15s;
}
.prefix-swatch:hover{transform:translateY(-1px);}
.prefix-swatch.is-active{
  border-color:#fff;
  box-shadow:0 0 0 1px var(--line) inset, 0 0 0 2.5px var(--blue);
}

.prefix-actions{
  display:flex;
  gap:10px;
  margin-top:20px;
}
.prefix-btn-save{
  flex:1;
  justify-content:center;
  padding:12px 18px;
  font-size:12.5px;
  letter-spacing:.8px;
  box-shadow:0 10px 20px rgba(28,111,224,.3);
}
.prefix-btn-save.is-loading{opacity:.6; pointer-events:none;}
.btn-prefix-reset{
  flex:0 0 auto;
  padding:12px 18px;
  border-radius:12px;
  border:1.5px solid var(--line);
  background:#f0f4fa;
  color:var(--navy-soft);
  font:inherit;
  font-size:12.5px;
  font-weight:700;
  letter-spacing:.5px;
  cursor:pointer;
  transition:background .2s, border-color .2s, color .2s;
}
.btn-prefix-reset:hover{background:#fdecec; border-color:#e2554f; color:#c0392b;}
.btn-prefix-reset.is-loading{opacity:.6; pointer-events:none;}

.prefix-form .field-hint{margin-top:14px; text-align:center;}
.prefix-form .modal-msg{margin-top:8px;}

@media (max-width: 760px){
  .profile-hero{flex-direction:column; text-align:center; padding:36px 24px;}
  .profile-info-col{display:flex; flex-direction:column; align-items:center;}
  .profile-stat{width:100%;}
  .profile-tabs-nav{justify-content:center;}
  /* On the stacked mobile layout the info column sits right under the
     cubes' absolute top/right coordinates (sized for the wide desktop
     card), so they'd otherwise land on top of the tabs/username text. */
  .profile-hero-decor{display:none;}
}

/* ---------- SERVER STATS (О сервере) ---------- */
.server-stats{
  display:grid;
  grid-template-columns:repeat(3, 1fr);
  gap:18px;
  margin-top:24px;
}
.stat-tile{
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:12px;
  padding:20px 18px;
  text-align:center;
}
.stat-value{
  font-size:20px;
  font-weight:800;
  color:var(--blue-dark);
  margin-bottom:6px;
}
.stat-label{
  font-size:12px;
  letter-spacing:.4px;
  color:#8493aa;
  text-transform:uppercase;
  font-weight:700;
}

/* ---------- MODS LIST (О сервере) ---------- */
.mods-section-title{
  font-size:13px;
  font-weight:800;
  letter-spacing:1px;
  text-transform:uppercase;
  color:var(--navy);
  margin:0 0 14px;
}
.mods-section-title-client{
  margin-top:32px;
}

/* Two-column row list rather than a wall of icon tiles — reads much
   better once you're past half a dozen mods, and leaves room for the
   name next to the icon instead of squeezed underneath it. */
.mods-list{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:2px 28px;
}
.mod-row{
  display:flex;
  align-items:center;
  gap:12px;
  padding:9px 6px;
  border-bottom:1px solid var(--line);
  border-radius:8px;
  transition:background .15s ease;
}
.mod-row:hover{
  background:#f6faff;
}
.mod-row .name{
  font-size:13px;
  font-weight:700;
  color:var(--navy);
}

/* No diamond frame here — just the icon on its own, with a thin colour
   tag standing in for the frame's colour-coding. Reads much calmer once
   you have 40+ of them in a row than a wall of identical rotated tiles. */
.mod-icon-plain{
  width:30px;height:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  flex-shrink:0;
}
.mod-icon-plain img{
  width:100%;height:100%;
  object-fit:contain;
  filter:drop-shadow(0 2px 5px rgba(30,60,120,.25));
}
/* Colour comes from an inline style (sampled per-icon in the Blade view)
   rather than a fixed set of modifier classes — every mod gets its own
   shade instead of one of six repeating presets. */
.mod-tag{
  width:4px;
  height:22px;
  border-radius:3px;
  flex-shrink:0;
}

/* Thaumcraft / Botania / Blood Magic get their own highlighted card so
   their addons read as a nested group instead of blending into the
   surrounding alphabet. */
.mod-family{
  grid-column:1 / -1;
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:14px;
  padding:16px 18px 8px;
  margin:6px 0 10px;
}
.mod-family-head{
  display:flex;
  align-items:center;
  gap:14px;
  padding-bottom:14px;
  margin-bottom:10px;
  border-bottom:1.5px solid #dbe9fb;
}
.mod-family-head .mod-icon-plain{
  width:44px;height:44px;
}
.mod-family-head .mod-tag{
  width:5px;
  height:34px;
}
.mod-family-head .name{
  font-size:15px;
  font-weight:800;
}
.mod-family-addons{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:0 24px;
}
.mod-family-addons .mod-row{
  padding:7px 4px;
  border-bottom-color:#dbe9fb;
}
.mod-family-addons .mod-row:hover{
  background:#eaf3ff;
}
.mod-family-addons .mod-icon-plain{
  width:24px;height:24px;
}
.mod-family-addons .mod-tag{
  height:16px;
}

.map-embed{
  margin-bottom:24px;
}
.map-embed-frame{
  position:relative;
  width:100%;
  aspect-ratio:16 / 9;
  min-height:420px;
  max-height:640px;
  border-radius:14px;
  overflow:hidden;
  background:#0f1b2d;
  box-shadow:var(--shadow);
}
.map-embed-frame iframe{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  border:0;
}
.map-embed-fallback{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:14px;
  flex-wrap:wrap;
  margin-top:12px;
  padding:12px 18px;
  border-radius:11px;
  background:#eaf3ff;
  border:1px solid #d5e6fb;
}
.map-embed-fallback p{
  margin:0;
  font-size:12.5px;
  line-height:1.5;
  color:#5b6b85;
  max-width:560px;
}
.map-embed-fallback a{
  display:inline-flex;
  align-items:center;
  gap:7px;
  padding:9px 16px;
  border-radius:9px;
  background:var(--blue);
  color:#fff;
  text-decoration:none;
  font-weight:700;
  font-size:12.5px;
  white-space:nowrap;
  flex-shrink:0;
  transition:background .2s ease, transform .2s ease;
}
.map-embed-fallback a:hover{
  background:var(--blue-dark);
  transform:translateY(-1px);
}
@media (max-width: 640px){
  .map-embed-frame{
    aspect-ratio:auto;
    min-height:280px;
    max-height:420px;
  }
  .map-embed-fallback{
    flex-direction:column;
    align-items:stretch;
    text-align:center;
  }
  .map-embed-fallback a{
    justify-content:center;
  }
}

.contact-grid{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:18px;
}
.contact-tile{
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:12px;
  padding:18px 20px;
}
.contact-label{
  font-size:10px;
  letter-spacing:1.5px;
  color:#93a1b8;
  font-weight:700;
  text-transform:uppercase;
  margin-bottom:6px;
}
.contact-value{
  font-size:15px;
  font-weight:700;
  color:var(--navy);
  margin-bottom:4px;
}
.contact-desc{
  font-size:12.5px;
  color:#8493aa;
  line-height:1.5;
}

/* ---------- CLAN PAGE (О клане) ---------- */
.clan-banner{
  max-width:920px;
  margin:0 auto 20px;
  border-radius:20px;
  overflow:hidden;
  position:relative;
  box-shadow:var(--shadow);
  z-index:3;
}
.clan-banner-bg{
  position:absolute;
  inset:0;
  background-size:cover;
  background-position:center 42%;
  transform:scale(1.02);
}
.clan-banner-overlay{
  position:absolute;
  inset:0;
  background:linear-gradient(180deg, rgba(10,12,26,.28) 0%, rgba(9,10,22,.72) 62%, rgba(7,8,18,.92) 100%);
}
.clan-banner-content{
  position:relative;
  z-index:1;
  padding:52px 40px 42px;
  text-align:center;
  color:#fff;
}
.clan-emblem{
  width:88px;
  height:auto;
  margin-bottom:10px;
  filter:drop-shadow(0 0 26px rgba(150,120,255,.55));
}
.clan-name{
  font-size:38px;
  font-weight:800;
  letter-spacing:4px;
  color:#fff;
  text-shadow:0 0 22px rgba(150,125,255,.6), 0 2px 12px rgba(0,0,0,.45);
}
.clan-tagline{
  max-width:560px;
  margin:12px auto 20px;
  font-size:14px;
  line-height:1.65;
  color:rgba(255,255,255,.8);
}
.clan-motto{
  display:inline-block;
  font-style:italic;
  font-weight:600;
  font-size:13.5px;
  letter-spacing:.2px;
  color:#e2d4ff;
  padding:10px 24px;
  border:1px solid rgba(202,175,255,.4);
  border-radius:30px;
  background:rgba(140,100,255,.14);
}

.clan-tabs-nav{
  display:flex;
  flex-wrap:wrap;
  gap:0 26px;
  margin-bottom:30px;
  border-bottom:1.5px solid var(--line);
}
.clan-tab-btn{
  background:none;
  border:none;
  font:inherit;
  font-weight:700;
  font-size:13px;
  letter-spacing:.4px;
  color:var(--navy-soft);
  padding:10px 2px 13px;
  margin-bottom:-1.5px;
  cursor:pointer;
  position:relative;
  transition:color .2s;
}
.clan-tab-btn:hover{color:var(--blue-dark);}
.clan-tab-btn.active{color:var(--navy);}
.clan-tab-btn.active::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:-1.5px;
  height:2px;
  background:var(--blue);
  border-radius:2px;
}

/* ----- Текст ГИБКИХ вкладок (WYSIWYG из /backend/clan/tabs — ClanTab::
   body) ----- "История" сейчас, плюс любые новые вкладки, которые
   админ/модератор добавит через тот же экран. "Обзор" и "Устав" сюда
   больше НЕ относятся — у них снова свои структурированные поля/таблицы
   (.clan-callout/.value-card ниже и .charter-block/.penalty-* в конце
   этого раздела), чтобы точно повторять прежнюю вёрстку: карточки
   «Заповедей», врезка «Завет», лестница штрафов со стрелками — из
   произвольного HTML такое надёжно не получить, а из своих таблиц можно
   отрисовать один в один. Для содержимого, которое ОСТАЁТСЯ свободным
   текстом (сплошной рассказ вроде "Истории"), правила ниже всё ещё по
   тегу (не по классу — санитайзер его не пропускает): нумерованный
   список рисуется сеткой карточек с крупным полупрозрачным номером (тот
   же приём, что у .value-card/.value-num, просто номер генерируется
   CSS-счётчиком), маркированный — аккуратный список с цветным маркером,
   цитата — тем же фиолетовым видом, что и врезка «Завет». */

/* ----- «Обзор» (ClanOverview/ClanValue, Backend\ClanOverviewController) ----- */
.clan-callout{
  background:linear-gradient(135deg,#f7f2ff,#ece2fb);
  border:1px solid #ded0f6;
  border-radius:14px;
  padding:22px 26px;
  margin:22px 0 30px;
}
.clan-callout-label{
  font-size:11px;
  letter-spacing:2px;
  text-transform:uppercase;
  font-weight:800;
  color:#7c5cd6;
  margin-bottom:8px;
}
.clan-callout p{
  margin:0;
  font-size:14px;
  line-height:1.7;
  color:#453a63;
}

.clan-values{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:16px;
  margin:14px 0 32px;
}
.value-card{
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:12px;
  padding:18px 20px;
  position:relative;
  overflow:hidden;
}
.value-num{
  position:absolute;
  top:8px; right:14px;
  font-size:26px;
  font-weight:800;
  color:rgba(47,141,255,.16);
  line-height:1;
}
.value-title{
  position:relative;
  z-index:1;
  font-size:13.5px;
  font-weight:800;
  color:var(--navy);
  margin:0 0 6px;
  padding-right:26px;
}
.value-desc{
  position:relative;
  z-index:1;
  margin:0;
  font-size:12.5px;
  line-height:1.55;
  color:#8493aa;
}

/* Тизер "Тайное знание" под «Заповедями» — первые 1-2 абзаца ПЕРВОЙ
   гибкой вкладки (обычно "История"), автоматически, без отдельного
   поля для тизера (см. sections/clan.blade.php) — кнопка ведёт на эту
   вкладку. Если гибких вкладок нет вообще, весь блок не рендерится. */
.clan-origin-teaser p{
  font-size:13.5px;
  line-height:1.7;
  color:#5b6b85;
  margin:0 0 14px;
}
.clan-read-more{
  background:none;
  border:none;
  padding:0;
  margin-top:4px;
  font:inherit;
  font-weight:700;
  font-size:13px;
  color:var(--blue-dark);
  cursor:pointer;
  transition:color .2s;
}
.clan-read-more:hover{color:var(--blue); text-decoration:underline;}

/* ----- «Устав» (ClanCharter/ClanCharterSection/ClanCharterItem/
   ClanPenaltyStep, Backend\ClanCharterController) ----- */
.clan-charter-kicker{
  font-size:11px;
  letter-spacing:2px;
  text-transform:uppercase;
  font-weight:800;
  color:#93a1b8;
  margin-bottom:8px;
}
.charter-block{margin:26px 0;}
.charter-block:first-of-type{margin-top:22px;}
.charter-block h3{
  margin:0 0 12px;
  font-size:14px;
  font-weight:800;
  color:var(--navy);
}
.penalty-scale{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:10px;
  margin-bottom:12px;
}
.penalty-step{
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:999px;
  padding:8px 16px;
  font-size:12.5px;
  font-weight:700;
  color:var(--navy);
}
.penalty-arrow{
  color:#b7c0d1;
  font-size:14px;
  font-weight:700;
}
.charter-induction{margin:0;}

/* ----- Гибкие вкладки (WYSIWYG) ----- */
.lore-article{max-width:720px; margin:0 auto; font-size:14px; line-height:1.8; color:#5b6b85;}
.lore-article h2{margin:30px 0 14px; font-size:19px; font-weight:800; color:var(--navy);}
.lore-article h2:first-child{margin-top:0;}
.lore-article h3{margin:24px 0 12px; font-size:15px; font-weight:800; color:var(--navy);}
.lore-article h3:first-child{margin-top:0;}
.lore-article p{margin:0 0 16px;}
.lore-article li{font-size:13.5px; line-height:1.65;}

.lore-article ul{margin:0 0 18px; padding-left:22px; display:flex; flex-direction:column; gap:9px;}
.lore-article ul li::marker{color:var(--blue-dark); font-weight:800;}

.lore-article ol{
  margin:0 0 24px;
  padding:0;
  list-style:none;
  counter-reset:lore-card;
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:14px;
}
.lore-article ol li{
  counter-increment:lore-card;
  position:relative;
  background:#f6faff;
  border:1px solid var(--line);
  border-radius:12px;
  padding:16px 44px 16px 18px;
  overflow:hidden;
}
.lore-article ol li::before{
  content:counter(lore-card, decimal-leading-zero);
  position:absolute;
  top:6px; right:14px;
  font-size:26px;
  font-weight:800;
  color:rgba(47,141,255,.16);
  line-height:1;
}

.lore-article strong{color:var(--navy); font-weight:800;}
.lore-article a{color:var(--blue-dark); font-weight:600; text-decoration:none;}
.lore-article a:hover{text-decoration:underline;}
/* Тот же фиолетовый градиент, что раньше был только у ручного блока
   "Завет" (.clan-callout) — теперь это универсальный вид цитаты/
   врезки для любой вкладки, доступный через кнопку "Blockquote" в
   тулбаре Quill. */
.lore-article blockquote{
  margin:26px 0;
  padding:20px 26px;
  background:linear-gradient(135deg,#f7f2ff,#ece2fb);
  border:1px solid #ded0f6;
  border-radius:14px;
  font-size:14.5px;
  font-style:italic;
  font-weight:600;
  color:#453a63;
  line-height:1.7;
}
.lore-article blockquote p:last-child{margin-bottom:0;}
/* Единственная картинка вкладки (ClanTab::image) — отдельно от текста,
   не часть отсанитайзенного body. */
.clan-tab-image{
  display:block;
  width:100%;
  max-width:420px;
  margin:8px auto 30px;
  border-radius:16px;
  filter:drop-shadow(0 14px 30px rgba(31,90,180,.18));
}

/* ----- Состав tab ----- */
.roster-intro{margin-bottom:22px;}
.roster-group{margin-bottom:28px;}
.roster-group:last-child{margin-bottom:0;}
.roster-group-title{
  display:flex;
  align-items:center;
  gap:10px;
  font-size:13px;
  font-weight:800;
  letter-spacing:.5px;
  text-transform:uppercase;
  color:var(--navy);
  margin-bottom:10px;
}
.roster-rank-dot{
  width:10px;height:10px;
  border-radius:50%;
  flex-shrink:0;
  /* Neutral fallback for a rank group added from the backend whose slug
     isn't one of the six .rank-* gradients below (those, being later in
     this file, still win for a recognized slug — same specificity, later
     wins) — so a brand-new rank still shows a dot/avatar instead of
     nothing. */
  background:linear-gradient(135deg,#cbd5e1,#8ea0b8);
}
.roster-count{
  margin-left:auto;
  font-size:11px;
  font-weight:700;
  color:#93a1b8;
  background:#f0f4fa;
  padding:3px 10px;
  border-radius:999px;
  text-transform:none;
  letter-spacing:0;
}
.roster-list{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:8px;
}
.roster-row{
  display:flex;
  align-items:center;
  gap:12px;
  padding:10px 12px;
  border-radius:10px;
  text-decoration:none;
  border:1px solid transparent;
  transition:background .15s, border-color .15s;
}
.roster-row:hover{background:#f6faff; border-color:var(--line);}
.roster-avatar{
  width:34px;height:34px;
  border-radius:10px;
  display:flex;
  align-items:center;
  justify-content:center;
  color:#fff;
  font-weight:800;
  font-size:13px;
  flex-shrink:0;
  position:relative;
  overflow:hidden;
  /* Same neutral fallback as .roster-rank-dot, same reasoning. */
  background:linear-gradient(135deg,#cbd5e1,#8ea0b8);
}
/* Real skin face crop for a roster member whose ник matches a registered
   site account (sections/clan.blade.php — $matchedUsers lookup), laid over
   the letter/rank-gradient fallback that's still underneath in the markup.
   Same two-layer technique as .skin-avatar-face, recalculated for this
   34px box: scale = 34/8 = 4.25, so bg-size = 64*4.25 = 272px, base head
   UV(8,8)*4.25 = -34px -34px, overlay UV(40,8)*4.25 = -170px -34px. Fixed
   px on purpose, not scaled by the skin's own resolution — see the long
   comment on .skin-avatar-face for why that's already correct for any
   square texture. */
.roster-avatar-face{
  position:absolute;
  inset:0;
  background-image:var(--skin-url), var(--skin-url);
  background-repeat:no-repeat, no-repeat;
  background-size:272px 272px, 272px 272px;
  background-position:-170px -34px, -34px -34px;
  image-rendering:pixelated;
  image-rendering:-moz-crisp-edges;
  image-rendering:crisp-edges;
}
.roster-name{
  font-size:13px;
  font-weight:700;
  color:var(--navy);
  flex:1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.roster-you-badge{
  font-size:9.5px;
  font-weight:800;
  letter-spacing:.5px;
  text-transform:uppercase;
  color:#fff;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  padding:3px 8px;
  border-radius:999px;
  flex-shrink:0;
}
.roster-link-icon{
  width:14px;height:14px;
  color:#b7c0d1;
  flex-shrink:0;
  transition:color .15s;
}
.roster-row:hover .roster-link-icon{color:var(--blue-dark);}

.rank-glava{background:linear-gradient(135deg,#ffd873,#e8a812);}
.rank-zamestitel{background:linear-gradient(135deg,#c9b6ff,#7c5cd6);}
.rank-ofitser{background:linear-gradient(135deg,#7fc0ff,#1c6fe0);}
.rank-doverennyy{background:linear-gradient(135deg,#5eead4,#0d9488);}
.rank-uchastnik{background:linear-gradient(135deg,#cbd5e1,#8ea0b8);}
.rank-novobranets{background:linear-gradient(135deg,#8fe0a8,#22a35a);}

@media (max-width: 640px){
  .info-card{padding:26px 20px 30px;}
  .contact-grid{grid-template-columns:1fr;}
  .server-stats{grid-template-columns:1fr;}
  .mods-list, .mod-family-addons{grid-template-columns:1fr;}
  .mod-family{padding:14px 14px 6px;}
  .clan-banner-content{padding:40px 22px 32px;}
  .clan-name{font-size:28px; letter-spacing:2px;}
  .clan-emblem{width:70px;}
  .clan-tabs-nav{gap:0 14px;}
  .clan-tab-btn{font-size:11.5px; padding:8px 2px 11px;}
  .clan-values, .lore-article ol, .roster-list{grid-template-columns:1fr;}
  .penalty-scale{gap:6px 8px;}
  .news-list-row{flex-direction:column;}
  .news-list-thumb{width:100%;height:170px;}
  .contacts-hero{min-height:auto; border-radius:20px; flex-direction:column;}
  .contacts-hero-light{max-width:100%; padding:32px 22px 24px;}
  .contacts-hero-light h2{font-size:23px;}
  .contacts-hero-light p{max-width:100%;}
  .contacts-cubes{display:none;}
  .contacts-hero-dark{padding:26px 22px 32px;}
  .discord-widget{max-width:100%;}
}

/* ---------- FOOTER ---------- */
footer{
  position:relative;
  z-index:3;
  border-top:1px solid var(--line);
  padding:26px 0 32px;
}
.footer-row{
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:center;
  gap:20px;
}
.footer-brand{
  display:flex;
  align-items:center;
  gap:10px;
  justify-self:start;
}
.footer-brand img{
  width:26px;height:26px;
  object-fit:contain;
  filter:grayscale(1) opacity(.55);
}
.footer-brand-text{
  line-height:1.35;
}
.footer-brand-text .fb1{
  font-size:11px;
  font-weight:700;
  letter-spacing:1px;
  color:#9aa5b8;
}
.footer-brand-text .fb2{
  font-size:11px;
  color:#b3bdcc;
}
.footer-social{
  display:flex;
  align-items:center;
  gap:10px;
  justify-self:center;
}
.footer-discord{
  display:flex;
  align-items:center;
  gap:8px;
  padding:9px 20px;
  border-radius:20px;
  background:var(--card);
  color:#5b6b85;
  text-decoration:none;
  font-size:12px;
  font-weight:700;
  letter-spacing:.5px;
  box-shadow:0 6px 16px rgba(31,90,180,.08);
  transition:color .2s, transform .2s;
}
.footer-discord:hover{color:var(--blue-dark); transform:translateY(-2px);}
.footer-discord svg{width:16px;height:16px;}
.footer-top{
  width:40px;height:40px;
  border-radius:50%;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  color:#fff;
  display:flex;align-items:center;justify-content:center;
  text-decoration:none;
  box-shadow:0 10px 22px rgba(28,111,224,.35);
  transition:transform .2s;
  flex-shrink:0;
  justify-self:end;
}
.footer-top:hover{transform:translateY(-3px);}
.footer-top svg{width:18px;height:18px;}

/* ---------- MODALS (Войти / Регистрация) ---------- */
.modal-backdrop{
  position:fixed;
  inset:0;
  background:rgba(20,30,50,.45);
  backdrop-filter:blur(2px);
  z-index:50;
  opacity:0;
  transition:opacity .2s ease;
}
.modal-backdrop.is-open{opacity:1;}

.modal{
  position:fixed;
  inset:0;
  z-index:51;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:20px;
  opacity:0;
  pointer-events:none;
  transition:opacity .2s ease;
}
.modal.is-open{
  opacity:1;
  pointer-events:auto;
}
.modal-card{
  background:var(--card);
  border-radius:20px;
  padding:34px 34px 28px;
  width:100%;
  max-width:400px;
  max-height:calc(100vh - 40px);
  overflow-y:auto;
  box-shadow:0 30px 60px rgba(20,40,80,.28);
  position:relative;
  transform:translateY(14px) scale(.97);
  transition:transform .25s cubic-bezier(.2,.9,.3,1.2);
}
.modal.is-open .modal-card{
  transform:translateY(0) scale(1);
}

/* Registration popup as one merged card, split into an art panel + the
   form panel (register.blade.php only) — matches the two-panel reference
   the user sent. The art panel's background is a painted gradient with the
   character artwork layered on top: the artwork itself is a transparent
   cutout (no opaque background of its own), so the gradient is what makes
   the panel read as a single filled rectangle instead of a floating
   sticker on empty space. On narrow screens there's no room for a second
   panel, so it collapses back to a single-column card (the .modal-card
   base rules above already give it the right padding/width there). */
.modal-card-register{
  overflow:hidden; /* clips both panels to the card's rounded corners */
}
.modal-card-body{
  max-height:calc(100vh - 40px);
  overflow-y:auto;
}
.modal-art{
  display:none;
}
@media (min-width:900px){
  .modal-card-register{
    display:flex;
    align-items:stretch;
    padding:0;
    max-width:740px;
    max-height:calc(100vh - 40px);
  }
  .modal-art{
    display:block;
    /* flex:1 (flex-basis:0) looked like an even split but isn't one once
       one side has padding and the other doesn't: with a 0 flex-basis,
       Chrome/Firefox distribute the *content-box* free space equally and
       then add each side's own padding on top — box-sizing:border-box on
       the item does NOT correct this (checked in a real browser). A
       percentage flex-basis doesn't have that problem — it resolves
       against the border-box container width, padding included on both
       sides — so this is a real 50/50 split with .modal-card-body below. */
    flex:0 1 50%;
    min-width:0;
    /* Dark background, on purpose — the artwork's magic-glow effects only
       read against a dark backdrop; on a light background they wash out
       and the art loses its "wow" factor (checked both side by side).
       Base blue is the site's own --blue/--blue-dark (#3f9bff/#1c6fe0),
       darkened a notch — at full brightness the flat blue competed with
       the artwork's own glow and made the transparent-PNG edges show. */
    background:
      radial-gradient(circle at 50% 65%, rgba(120,190,255,.2), transparent 55%),
      url('../images/modal/register-art.png') center 58%/120% no-repeat,
      linear-gradient(135deg, #2c74c9 0%, #10336f 100%);
  }
  .modal-card-register .modal-card-body{
    flex:0 1 50%; /* same fix as .modal-art — see comment there */
    min-width:0;
    padding:34px 34px 28px;
  }
}
.modal-close{
  position:absolute;
  top:16px; right:16px;
  width:30px;height:30px;
  border-radius:50%;
  border:none;
  background:#f0f4fa;
  color:#8493aa;
  font-size:18px;
  line-height:1;
  cursor:pointer;
  transition:background .2s, color .2s;
}
.modal-close:hover{background:#e2ecfb; color:var(--blue-dark);}
.modal-title{
  margin:0 0 4px;
  font-size:20px;
  font-weight:800;
  color:var(--navy);
}
.modal-subtitle{
  margin:0 0 22px;
  font-size:13px;
  color:#93a1b8;
}
.modal-form{
  display:flex;
  flex-direction:column;
  gap:10px;
}
.modal-field{
  display:flex;
  flex-direction:column;
  gap:6px;
  font-size:12px;
  font-weight:700;
  letter-spacing:.3px;
  color:#5b6b85;
  text-transform:uppercase;
}
.modal-field input{
  font:inherit;
  text-transform:none;
  font-weight:400;
  padding:11px 14px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:#f8fafd;
  color:var(--navy);
  transition:border-color .2s, background .2s;
}
.modal-field input:focus{
  outline:none;
  border-color:var(--blue);
  background:#fff;
}
.modal-field input::placeholder{
  color:#b7c3d6;
  opacity:1;
}
.field-error{
  min-height:14px;
  font-size:11px;
  font-weight:600;
  text-transform:none;
  letter-spacing:0;
  color:#e2554f;
  /* A long message (the skin-dimensions error especially — it names the
     bad size right in the text) must wrap inside whatever box it's in
     instead of forcing that box wider — see .skin-upload-form below for
     the specific case that surfaced this. */
  overflow-wrap:break-word;
  word-break:break-word;
}
.field-error:empty{min-height:0;}
.field-hint{
  min-height:14px;
  font-size:11px;
  font-weight:600;
  text-transform:none;
  letter-spacing:0;
  color:#93a1b8;
}
.field-hint:empty{min-height:0;}
.field-hint.is-success{color:#2f9c66;}
.field-hint.is-error{color:#e2554f;}

/* "Логин" field on the register popup — input + reserved "Проверить"
   button for a future nickname-availability check against the users
   table. Everything else about .modal-field still applies (the row is
   just one more stacked child, in place of a bare <input>). */
.modal-field-row{
  display:flex;
  align-items:stretch;
  gap:8px;
}
.modal-field-row input{flex:1 1 auto; min-width:0;}
.btn-check-login{
  flex:0 0 auto;
  padding:0 14px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:#f0f4fa;
  color:var(--blue-dark);
  font:inherit;
  font-size:11px;
  font-weight:700;
  letter-spacing:.3px;
  text-transform:uppercase;
  white-space:nowrap;
  cursor:pointer;
  transition:background .2s, border-color .2s;
}
.btn-check-login:hover{background:#e2ecfb; border-color:var(--blue);}
.btn-check-login:disabled{opacity:.6; cursor:default; pointer-events:none;}

/* Cloudflare Turnstile widget (session notes #85) — the .cf-turnstile div
   itself is replaced by Cloudflare's own iframe at render time, this only
   centers that iframe inside the modal's flex column (.modal-form already
   gives it consistent top/bottom spacing via its own gap, same as every
   other field). Replaces the old dashed-border placeholder box that used
   to sit here (.modal-captcha-slot) — see login.blade.php/register.blade.php. */
.cf-turnstile{display:flex; justify-content:center;}

/* Consent checkboxes (rules/privacy policy + data processing) below the
   captcha slot — plain rows, not .modal-field (no uppercase label / no
   column layout, just checkbox + wrapping text side by side). */
.modal-checkbox{
  display:flex;
  align-items:flex-start;
  gap:10px;
  font-size:12px;
  font-weight:400;
  line-height:1.55;
  color:#5b6b85;
  cursor:pointer;
}
.modal-checkbox input{
  flex-shrink:0;
  width:16px; height:16px;
  margin-top:2px;
  accent-color:var(--blue);
  cursor:pointer;
}
.modal-checkbox a{color:var(--blue-dark); font-weight:600; text-decoration:none;}
.modal-checkbox a:hover{text-decoration:underline;}
.modal-checkbox + .field-error{margin-top:-6px;}
.modal-submit{
  width:100%;
  justify-content:center;
  margin-top:6px;
}
.modal-msg{
  min-height:18px;
  margin:0;
  font-size:12.5px;
  font-weight:600;
  text-align:center;
}
.modal-msg.is-success{color:#2f9c66;}
.modal-msg.is-error{color:#e2554f;}
.modal-switch{
  margin:10px 0 0;
  text-align:center;
  font-size:12.5px;
  color:#93a1b8;
}
.modal-switch button{
  background:none;
  border:none;
  padding:0;
  font:inherit;
  font-weight:700;
  color:var(--blue-dark);
  cursor:pointer;
}

/* "Забыли пароль?" — login popup only, sits right under .modal-switch. */
.modal-forgot{
  margin:6px 0 0;
  text-align:center;
  font-size:12px;
}
.modal-forgot-link{
  color:var(--blue-dark);
  font-weight:600;
  text-decoration:none;
}
.modal-forgot-link:hover{text-decoration:underline;}

.modal-submit.is-loading .btn-label{opacity:.5;}

body.modal-open{overflow:hidden;}

@media (max-width: 700px){
  .footer-row{grid-template-columns:1fr; justify-items:center; text-align:center; gap:16px;}
  .footer-brand{justify-self:center;}
}

@media (max-width: 1100px){
  .mods{display:none;}
  nav ul{gap:16px;}
  nav a{font-size:11px;}
  .hero-title{font-size:38px;}
  .hero-sub{font-size:17px;letter-spacing:6px;}
  .island{width:33%;}
}
@media (max-width: 860px){
  .wrap{padding:0 20px;}
  nav ul{display:none;}
  .hero{flex-direction:column;gap:24px;padding-top:10px;}
  .island{width:82%;max-width:400px;margin:0;}
  .news-grid{grid-template-columns:1fr;}
}
@media (max-width: 480px){
  .nav-buttons{gap:8px;}
  .btn-register{padding:9px 12px; font-size:11px;}
  .btn-login{padding:9px 14px; font-size:11px;}
  .btn-login svg{display:none;}
  .modal-card{padding:28px 22px 24px;}
}

/* ---------- NEWS ARTICLE PAGE (/news/{slug}, resources/views/news/show.
   blade.php) — session notes #45. A real standalone page (own route, not
   an AJAX section), so it reuses the same .wrap/header/hero/footer shell
   as home.blade.php but its own content below the hero. ---------- */
.news-article{
  max-width:900px;
  margin:70px auto 40px;
  position:relative;
  z-index:3;
}
.news-article-back{
  display:inline-flex;
  align-items:center;
  gap:8px;
  margin-bottom:18px;
  padding:11px 20px 11px 16px;
  border-radius:11px;
  background:var(--card);
  box-shadow:var(--shadow);
  color:var(--blue-dark);
  text-decoration:none;
  font-weight:700;
  font-size:13px;
  transition:background .2s ease, transform .2s ease, box-shadow .2s ease;
}
.news-article-back svg{width:16px;height:16px; transition:transform .2s ease;}
.news-article-back:hover{
  background:#eaf3ff;
  color:var(--blue);
  transform:translateX(-3px);
  box-shadow:0 14px 28px rgba(31,90,180,.14);
}
.news-article-back:hover svg{transform:translateX(-2px);}
.news-article-card{
  background:var(--card);
  border-radius:18px;
  box-shadow:var(--shadow);
  overflow:hidden;
}
.news-article-cover{
  position:relative;
  height:340px;
}
.news-article-cover img{width:100%;height:100%;object-fit:cover;display:block;}
.news-article-body{padding:32px 36px 40px;}
.news-article-body h1{
  margin:0 0 16px;
  font-size:26px;
  font-weight:800;
  color:var(--navy);
  line-height:1.3;
}
.news-article-meta{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:18px;
  padding-bottom:20px;
  margin-bottom:24px;
  border-bottom:1px solid var(--line);
}
.news-article-author{
  font-size:12px;
  color:#93a1b8;
  font-weight:600;
}
.news-article-text{
  font-size:15px;
  line-height:1.8;
  color:#4c5a72;
}
.news-article-text p{margin:0 0 16px;}
.news-article-text p:last-child{margin-bottom:0;}

@media (max-width:640px){
  .news-article-cover{height:200px;}
  .news-article-body{padding:24px 20px 30px;}
}

/* ---------- IMPERSONATION BANNER ---------- */
/* Shown across the top of every public page while a /backend admin is
   browsing the site logged in AS someone else (Backend\UserController::
   impersonate() sets session('impersonate_admin_id'), see layouts/app.
   blade.php). "Вернуться в бэкенд" posts to the ordinary /logout route —
   AuthController::logout() special-cases this session flag to log out
   only the public 'web' guard and send the admin back to /backend instead
   of doing a full session teardown (which would also sign them out of
   /backend — same class of bug as the one EnsureAdmin had to avoid, see
   session notes #44/#45). */
.impersonate-bar{
  position:sticky;
  top:0;
  z-index:50;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:14px;
  flex-wrap:wrap;
  padding:10px 20px;
  background:#2f3b52;
  color:#fff;
  font-size:13px;
  font-weight:600;
  text-align:center;
}
.impersonate-bar strong{color:#ffd479;}
.impersonate-bar form{margin:0;}
.impersonate-bar button{
  padding:6px 14px;
  border:none;
  border-radius:8px;
  background:#ffd479;
  color:#2f3b52;
  font:inherit;
  font-weight:700;
  font-size:12.5px;
  cursor:pointer;
}
.impersonate-bar button:hover{background:#ffc94f;}

/* Custom override of Laravel's pagination view — see
   resources/views/vendor/pagination/tailwind.blade.php. No public page
   uses ->paginate()/->links() yet (see sections/news.blade.php — it
   just lists every published item), but if one ever does, this makes
   sure it renders styled instead of as unstyled Tailwind markup —
   same `.pgn` classes as backend.css, just this stylesheet's own
   tokens (no `b-` prefix). */
.pgn{display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px 16px;}
.pgn-info{font-size:12.5px; color:var(--navy-soft);}
.pgn-links{display:flex; align-items:center; flex-wrap:wrap; gap:4px;}
.pgn-link{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:30px;
  height:30px;
  padding:0 8px;
  border-radius:7px;
  font-size:12.5px;
  font-weight:600;
  color:var(--navy-soft);
  text-decoration:none;
  border:1px solid transparent;
}
a.pgn-link:hover{background:#eaf3ff; color:var(--blue-dark);}
.pgn-link.is-current{background:var(--blue); color:#fff;}
.pgn-link.is-disabled{color:#b9c3d6; cursor:default;}
.pgn-link.is-ellipsis{color:#b9c3d6; padding:0 4px; min-width:auto;}

/* ---------- GUIDES (Гайды и моды) ----------
   Список модов — карточная сетка в 1-2 колонки (.guide-grid/.guide-card),
   тот же язык, что у .news-list/.news-list-row (белая карточка-контейнер
   с тенью, кликабельные строки-ссылки), но с своей раскладкой (grid, не
   flex-column) и своей разметкой — own namespace, ничего из news не
   переиспользуется напрямую. Клик — обычный переход по ссылке
   (App\Http\Controllers\GuideController), с AJAX-подменой поверх него
   через data-guide, тем же приёмом, что и data-news у новостей (см.
   loadGuideArticle() в app.js). */
.guides-lead{max-width:900px; margin-left:auto; margin-right:auto;}
.guide-grid{
  max-width:900px;
  /* 20px снизу — тот же отступ перед футером, что у .info-card/.news-list
     в остальных разделах (у футера самого нет своего margin-top, только
     border-top, так что этот отступ целиком на разделе). Раньше был
     0 (только margin:0 auto), из-за чего сетка почти вплотную упиралась
     в футер — сюда как раз и жаловался пользователь. */
  margin:0 auto 20px;
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  gap:16px;
}
.guide-card{
  display:flex;
  align-items:center;
  gap:16px;
  background:var(--card);
  border-radius:16px;
  box-shadow:var(--shadow);
  padding:20px;
  text-decoration:none;
  color:inherit;
  transition:transform .2s ease, box-shadow .2s ease;
}
.guide-card:hover{
  transform:translateY(-3px);
  box-shadow:0 16px 34px rgba(31,90,180,.16);
}
.guide-card.is-soon{opacity:.8;}
.guide-card-icon{
  flex:0 0 auto;
  display:flex;
  align-items:center;
  justify-content:center;
  width:56px; height:56px;
  border-radius:14px;
  background:#f6faff;
  box-shadow:0 0 0 1px var(--line) inset;
}
.guide-card-icon img{width:34px; height:34px; object-fit:contain;}
.guide-card-icon img.pixel-icon{image-rendering:pixelated;}
.guide-card-body{flex:1; min-width:0;}
.guide-card-body h3{
  margin:0 0 6px;
  font-size:15.5px;
  font-weight:800;
  color:var(--navy);
  display:flex;
  align-items:center;
  gap:8px;
}
.guide-card-body p{
  margin:0;
  font-size:12.5px;
  line-height:1.55;
  color:#8493aa;
}
.guide-card-arrow{
  flex:0 0 auto;
  width:18px; height:18px;
  color:#b9c3d6;
  transition:transform .2s ease, color .2s ease;
}
.guide-card:hover .guide-card-arrow{transform:translateX(4px); color:var(--blue-dark);}
.guide-soon-badge{
  font-size:9.5px;
  font-weight:800;
  letter-spacing:.4px;
  text-transform:uppercase;
  color:#93a1b8;
  background:#eef2f8;
  border-radius:5px;
  padding:2px 6px;
}

@media (max-width: 640px){
  .guide-grid{grid-template-columns:1fr;}
}

/* ---------- GUIDE ARTICLE (страница мода/материала, /guides/{mod}[/{material}]) ----------
   Тот же принцип, что и у .news-article/.news-article-back — своя шапка
   (иконка + название) вместо обложки-картинки новости, дальше либо
   .guide-grid (материалы мода) / калькулятор (сам материал), либо
   .guide-placeholder. 20px снизу — тот же отступ перед футером, что у
   .guide-grid выше (та же причина: раньше 0, футер вплотную). */
.guide-article{max-width:900px; margin:0 auto 20px;}
.guide-article-back{
  display:inline-flex;
  align-items:center;
  gap:8px;
  margin-bottom:18px;
  font-size:13px;
  font-weight:700;
  color:var(--navy-soft);
  text-decoration:none;
}
.guide-article-back svg{width:16px; height:16px;}
.guide-article-back:hover{color:var(--blue-dark);}
/* Раньше .guide-article-back шёл ПЕРЕД .news-heading, и там был урезанный
   override его верхнего отступа (8px вместо стандартных 70px) — из-за
   этого у самой ссылки не было отступа от хиро, и она наезжала на его
   плавающие картинки (жалоба пользователя со скриншотом). Теперь .news-
   heading — первый элемент на guides/show.blade.php и guides/material.
   blade.php (его собственные 70px и дают отступ от хиро), а .guide-
   article-back идёт следом, со своим обычным margin-bottom выше — никакого
   override здесь больше не нужно. .guide-article-head/.guide-article-icon
   (иконка + <h1>) больше нигде не используются — обе страницы (мод и
   материал) теперь используют .news-heading вместо них. */

/* Тот же плейсхолдер-паттерн, что и .profile-placeholder — своя копия
   класса (own namespace), а не переиспользование, по той же причине. */
.guide-placeholder{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  padding:34px 20px;
  text-align:center;
  background:#f6faff;
  border:1px dashed var(--line);
  border-radius:12px;
  color:#8a97ad;
  margin-bottom:24px;
}
.guide-placeholder svg{width:26px; height:26px; opacity:.7;}
.guide-placeholder-text{font-size:13px; font-weight:600; max-width:480px;}

/* Раунд #105 — шрифт для чисел аспектов калькулятора Centi-Vis ниже
   (.cv-hud-value), файл прислан пользователем напрямую (public/fonts/
   minecraft.ttf). Внутреннее имя в самом файле — "Minecraft Rus"
   (Regular, с кириллицей), но семейство для CSS называем своим именем
   ('MinecraftRus') — так безопаснее, если у пользователя на компьютере
   уже установлен системный шрифт с похожим именем: тогда браузер бы мог
   найти локальный, а не файл сайта, и они не обязаны совпадать 1:1.
   Только Regular-начертание есть в файле, поэтому font-weight:400 везде,
   где используется (см. .cv-hud-value — жирный текст пиксельным
   шрифтом браузер только "смазал" бы, а не утолщил по-настоящему). */
@font-face{
  font-family:'MinecraftRus';
  src:url('/fonts/minecraft.ttf') format('truetype');
  font-weight:400;
  font-style:normal;
  font-display:swap;
}

/* ---------- Калькулятор Centi-Vis (Thaumcraft) ----------
   Чистый клиентский расчёт (см. app.js) — ничего не отправляется на
   сервер, поэтому обычная карточка-панель, а не форма.

   ИСТОРИЯ ФОНА КАРТОЧКИ (важно перечитать перед следующей правкой):
   1) узел на светлом фоне + отдельный тёмный круг только под ним —
      отклонено пользователем ("это бред, либо весь блок тёмный делай,
      либо думай что-то ещё");
   2) вся карточка тёмная целиком (Round #98) — решило контраст (бледные
      аспекты вроде Aer/Ordo хорошо читаются на любом тёмном фоне) и
      Round #104 её же полировал (синеватая тень/рамка в духе сайта);
   3) Round #105 — пользователь попросил вернуть светлую карточку "как
      было" (сплошная тёмная не подошла эстетически — единственная
      тёмная карточка на светлом сайте), контраст под бледные аспекты
      дал отдельный тёмный "экран" (.cv-node-panel) под HUD+узлом;
   4) Round #106 — пользователь отклонил и экран тоже ("убери вообще
      чёрный фон у узлов"), попросил обойтись ТЕНЬЮ. .cv-node-panel
      убран целиком (класса больше нет). Контраст под бледные аспекты
      теперь даёт только filter:drop-shadow — у .cv-node-stage и
      .cv-hud-icon ниже (drop-shadow обводит alpha-силуэт независимо от
      цвета заливки, поэтому работает и для белых/бледных аспектов).
   ТЕКУЩЕЕ решение: карточка светлая (var(--card)/var(--shadow)/
   var(--line), тот же приём, что у любой другой карточки сайта), узел и
   иконки — без всякой подложки, только тень. */
.cv-calc{
  background-color:var(--card);
  /* Round #118: просьба пользователя — украсить карточку присланным
     ассетом (лист рун Таумономикона из мода, скрин пользователя — 256×256,
     чёрные штрихи с прозрачным фоном), "чтобы как будто это книга".
     Обычный background-image, а не отдельный элемент/псевдоэлемент:
     background у элемента и так всегда обрезается его собственной рамкой
     (border-box) независимо от overflow — картинка не может вылезти за
     скруглённые углы карточки или перекрыть что-то кликабельное (фон не
     создаёт интерактивного слоя), а значит не мешает всплывающему списку
     аспектов (.cv-aspect-picker-list, position:absolute, z-index:12,
     может быть выше самой карточки) — тот по-прежнему рисуется поверх
     всего без каких-либо новых ограничений по overflow/stacking context
     от этого правила.
     Сам файл (public/images/textures/thaumonomicon-runes.png) — не
     оригинал, а предобработанная копия: повёрнута на -9° (просьба
     "небрежно", как будто лист лёг вкривь) и альфа-канал штрихов снижен
     до ~16% от исходного (в оригинале почти непрозрачные чёрные линии,
     на белой карточке поверх текста это было бы слишком ярко/отвлекало
     бы от самого калькулятора — теперь читается как выцветший узор на
     старой бумаге, а не отдельная картинка). Прижата в верхний правый
     угол с уходом за края (background-position с отрицательным
     смещением) — часть рисунка обрезается рамкой карточки, отсюда и
     ощущение "небрежности" (будто лист подложен, а не отцентрован и
     красиво вписан). На узких экранах (media query внизу файла) размер и
     смещение уменьшены пропорционально — тот же эффект, без наезда на
     контент при меньшей ширине карточки. */
  background-image:url('/images/textures/thaumonomicon-runes.png');
  background-repeat:no-repeat;
  background-position:right -40px top -40px;
  background-size:320px 320px;
  border:1px solid var(--line);
  border-radius:16px;
  padding:26px 26px 30px;
  box-shadow:var(--shadow);
}
/* Раунд #105: заголовок + короткое пояснение сверху — раньше здесь был
   абзац .cv-calc-lead (см. старую историю в git/session-notes: убран по
   просьбе пользователя ещё в Round #95, "вообще можно убрать"), теперь
   пользователь попросил вернуть "плашку с текстом" сверху. Текст новый
   (старый описывал давно неактуальный интерфейс — 6 фиксированных полей
   вместо нынешних 48 аспектов свободным набором), но место и роль те
   же: коротко объяснить, что считает калькулятор, одним заголовком и
   одной строкой, а не длинным текстом. */
.cv-calc-head{margin:0 0 22px;}
.cv-calc-title{
  margin:0 0 6px;
  font-size:17px;
  font-weight:800;
  color:var(--navy);
}
.cv-calc-lead{
  margin:0;
  font-size:13.5px;
  line-height:1.6;
  color:var(--navy-soft);
}
/* Round #114: .cv-field/.cv-node-options/.cv-node-btn (обёртка и кнопки
   выбора "Тип узла" — Яркий/Обычный/Блёклый/Увядающий) убраны целиком —
   формула больше не домножает на модификатор узла (см. историю в app.js
   у calcCentivis()/CV_CENTIVIS_TABLE и в centivis-calculator.blade.php),
   отдельная кнопка-модификатор больше не нужна, а не просто отключена от
   расчёта — пользователь прямо попросил убрать её из интерфейса. */
.cv-field-label{
  display:block;
  font-size:12px;
  font-weight:700;
  letter-spacing:.3px;
  color:var(--navy-soft);
  text-transform:uppercase;
  margin-bottom:10px;
  grid-row:1; /* см. .cv-body ниже — обе оставшиеся метки ("Аспекты узла", "Итоговый узел") внутри него */
}
/* Слева — добавленные аспекты узла (свободный набор из всех 48, каждый
   в любом количестве), справа — вычисленный узел (6 первичных), между
   ними стрелка. auto в центре — стрелке своя фиксированная ширина, две
   остальные колонки делят место поровну. На мобильных складываются в
   столбец (media-query ниже), стрелка там разворачивается на 90°.

   Раунд #106: именованные ряды (grid-template-rows), а не просто 2
   колонки без рядов — нужно, чтобы .cv-node-stage слева и справа
   оказались "на одном уровне" (просьба пользователя), хотя над ними в
   левой колонке заметно больше контента (строка выбора аспекта + список
   уже добавленных), чем в правой (только сама HUD-группа результата).
   .cv-col-input/.cv-col-output — subgrid по рядам родителя (см. ниже),
   каждый элемент внутри колонки явно приписан к своему ряду.

   Round #117: рядов теперь 5, а не 4 — .cv-add-row (строка выбора
   аспекта) получила СВОЙ отдельный ряд 2 вместо того, чтобы делить ряд
   2 вместе с чипами внутри общего .cv-col-pre-node, как было с Round
   #106 по #116. Причина — Round #116 добавил .cv-col-pre-node/.cv-hud
   align-self:end (см. их правила ниже), чтобы убрать лишний отступ
   ПЕРЕД узлом, когда чужая колонка выше; но пока строка выбора была
   частью того же блока, align-self:end двигал ВЕСЬ блок целиком вниз,
   утаскивая с собой и саму строку выбора аспекта каждый раз, когда
   справа аспектов оказывалось больше, чем помещается в одну строку
   слева (жалоба пользователя: "смещается элемент вниз") — строка
   выбора должна оставаться на месте под меткой, а align-self:end имеет
   смысл только для самого списка аспектов (что рядом с узлом), не для
   элементов управления над ним.

   Ряды теперь: .cv-field-label → 1, .cv-add-row (только у левой
   колонки, у правой на этом месте просто ничего нет) → 2, .cv-col-
   pre-node/.cv-hud[data-cv-result] → 3, .cv-node-stage → 4,
   .cv-result-total → 5. Ряд 3 по-прежнему автоматически становится
   высотой под самый высокий вариант между колонками (то список чипов
   слева, то результат справа — какой из них длиннее, зависит от
   конкретного набора аспектов, не всегда одна и та же колонка), и оба
   .cv-node-stage в ряду 4 стартуют с одной Y — та же структурная
   гарантия, что и раньше, просто без побочного смещения строки выбора.
   column-gap вместо gap — вертикальные отступы между рядами по-прежнему
   дают собственные margin у элементов (как и раньше), а не одинаковый
   gap ряда, который добавил бы отступ и там, где надо, и там, где уже
   есть свой margin. */
.cv-body{
  display:grid;
  grid-template-columns:1fr auto 1fr;
  grid-template-rows:auto auto auto auto auto;
  align-items:start;
  column-gap:20px;
}
.cv-col-input, .cv-col-output{
  display:grid;
  grid-row:1 / -1;
  grid-template-rows:subgrid;
  min-width:0;
}

/* Модель узла — просто позиционирующая обёртка. Раунд #106: у неё
   (и вообще у всей карточки) больше нет тёмной подложки — пользователь
   попросил убрать чёрный фон совсем и обойтись тенью ("Сделай Просто
   тень для узла и для аспектов"), см. filter:drop-shadow ниже вместо
   .cv-node-panel из Round #105 (класс убран целиком).

   Раунд #107: та первая версия тени была слишком слабой — пользователь
   прислал реальный скриншот (не мою тестовую заглушку): "Не вижу теней
   для узлов и аспектов". Причина — у самого спрайта узла (и иконок
   аспектов) края и так мягкие/перистые (glow-эффект, запечённый в самой
   картинке), поэтому предыдущая тень (мягкая, симметричная, того же
   порядка блюра, что и собственный ореол спрайта) визуально сливалась с
   этим ореолом и терялась. Решение — ОДНА тень со смещением ВНИЗ,
   заметно темнее и без размытия "вширь" — она ложится под узлом
   отдельным тёмным пятном, а не растворяется в его собственном свечении
   по контуру. Раньше было ещё и лёгкое синее "сияние" (0 0 10px
   rgba(47,141,255,...)) — убрано: та же судьба, тонуло в собственном
   цветном ореоле спрайта, значимого эффекта не добавляло.

   Внутри — ДВА наложенных друг на друга слоя БЕЗ сдвига (просьба
   пользователя: "один ассет должен быть наложен на другой без сдвига")
   — одинаковый размер и позиция, разница только в цвете и фазе анимации
   (animation-delay у -b), чтобы не мигали абсолютно в такт и не
   читались как один плоский слой, даже когда цвет совпадает (один
   аспект на узле).

   mix-blend-mode здесь теперь multiply, а не screen (был таким, пока
   карточка была тёмной) — screen осветляет, складывая цвет слоя со
   ВСЕМ, что под ним, вплоть до фона страницы: на тёмном фоне это давало
   красивое "сложение света", а на светлом/белом фоне (как теперь)
   screen с белым задником даёт ровно белый цвет независимо от цвета
   слоя — оба слоя стали бы попросту невидимыми. multiply, наоборот,
   умножает цвет на подложку: на белом фоне слой отображается своим
   настоящим цветом без искажения, а в зоне пересечения с другим слоем
   цвета перемножаются (получается более тёмный, "стеклянный" оттенок) —
   тот же эффект "два цвета видны одновременно", но корректно работающий
   на любом фоне, а не только на тёмном.

   Раунд #108: та же добавка, что и у .cv-hud-icon (см. её комментарий) —
   вторая тень БЕЗ смещения и почти без размытия (0 0 2px), плотная
   тёмная обводка по контуру, вдобавок к тени-подложке снизу — на самых
   бледных узлах (когда оба слоя близки к белому) одной только тени
   снизу было недостаточно, чтобы узел цельно читался на белой карточке.

   Раунд #109: пользователь прислал реальный скриншот сайта — тени по
   Round #108 не видно вообще ни у узла, ни у иконок ("Не вижу теней для
   узлов и аспектов"). Проверка в реальном браузере (headless Chromium)
   показала: у .cv-hud-icon (см. её комментарий ниже) тень не рисовалась
   СОВСЕМ — не "слабо", а буквально нулевым результатом, потому что
   filter:drop-shadow и mask-image были заданы на одном и том же
   элементе, а в этой связке браузер тень от такого элемента не рисует
   вовсе (проверено изолированным тестом: тот же элемент без mask-image —
   тень рисуется нормально; тот же mask, но filter на ОБЁРТКЕ, а не на
   самом замаскированном элементе — тоже рисуется нормально). У
   .cv-node-stage это сразу было устроено верно (filter здесь, на
   обёртке, а mask — на дочерних .cv-node-visual-a/-b), так что здесь
   рисовалась настоящая, но просто СЛАБАЯ тень — значения увеличены
   ниже, раз сам механизм подтверждён рабочим.

   Раунд #110: filter перенесён С САМОЙ .cv-node-stage НА новую
   .cv-node-core (обёртка вокруг содержимого самого шара узла — слоёв
   .cv-node-visual и акцента .cv-node-type-accent, см. их комментарии
   ниже). Причина — .cv-node-stage теперь хост ещё и для кольца
   пробуждённого узла (см. .cv-node-energized-ring ниже, только у
   правого/итогового узла): filter на родителе группирует ВСЁ его
   содержимое в один слой перед тем, как обвести тенью (см. комментарий
   у .cv-hud-icon-shadow про то же самое свойство), а тень нужна только
   самому шарику узла — форма кольца угловатая, лучистая, и её силуэт в
   общей тени выглядел бы чужеродно (тёмные зубчатые лучи вместо
   аккуратной тени под круглым узлом).

   Раунд #111: z-index у .cv-node-core и кольца поменяны местами —
   пользователь: «сделай этот ассет поверх остальных, а то он позади
   где-то» (в Round #110 кольцо специально положили ПОД узел, лучи вокруг
   шарика — не подошло, кольцо должно быть видно целиком, поверх узла).

   Раунд #113: пользователь прислал сам мод (Thaumcraft-1.7.10-4.2.3.5.
   jar) и попросил свериться с настоящим рендером узлов и переделать
   сайт под него. Декомпилированный TileNodeRenderer.renderNode()
   (thaumcraft/client/renderers/tile/) показал: в игре КАЖДЫЙ добавленный
   на узел аспект рисуется ОТДЕЛЬНЫМ слоем СВОЕГО цвета (aspect.
   getColor() без подмешивания к другим, слои друг на друге через
   аддитивное сложение), а не двумя усреднёнными тонами, как было здесь
   раньше (.cv-node-visual-a/-b, см. историю ниже) — см. подробности у
   .cv-node-visual. Заодно нашлась ранее не учитываемая деталь: поверх
   шара в игре ВСЕГДА рисуется ещё один маленький акцентный слой по
   "типу" узла (NodeType — NORMAL/UNSTABLE/DARK/TAINTED/HUNGRY/PURE,
   свойство ОТДЕЛЬНОЕ от NodeModifier/Увядающий-Бледный-Яркий, которое
   уже учтено кнопками узла) — калькулятор его никак не выбирает, так что
   воспроизведён только вариант NORMAL, см. .cv-node-type-accent ниже. */
.cv-node-stage{
  width:96px; height:96px;
  margin:8px auto 0;
  position:relative;
  grid-row:4; /* Round #117 (было 3), см. .cv-body */
}
.cv-node-core{
  position:relative;
  width:100%; height:100%;
  z-index:1; /* под кольцом пробуждённого узла (z-index:2, см. .cv-node-energized-ring) — сам узел, кольцо поверх него */
  filter:drop-shadow(0 0 1.5px rgba(15,23,42,.75)) drop-shadow(0 0 4px rgba(15,23,42,.5)) drop-shadow(0 8px 12px rgba(15,23,42,.45));
}

/* Спрайт — 32-кадровый (public/images/nodes/node-glow.png, строка/
   "strip 0" НАСТОЯЩЕГО листа ассетов игры misc/nodes.png — сверено
   декомпиляцией TileNodeRenderer.renderNode() в Round #113, байт-в-байт
   тот же файл, что и в моде), нейтральный серо-белый пульсирующий
   шарик, перекрашиваемый через mask-image + background-color, тем же
   приёмом, что и иконки аспектов.

   Раунд #113: слой — ПО ОДНОМУ НА КАЖДЫЙ АСПЕКТ узла (было — ровно два
   слоя .cv-node-visual-a/-b с усреднённым в 2 тона цветом, см. историю
   ниже), это уже не фиксированная пара классов, а переменное число
   ОДИНАКОВЫХ по классу элементов — строит и перестраивает их JS
   (cvRenderNodeLayers() в app.js, вызывается из calcCentivis()) при
   каждом пересчёте: по слою на каждый введённый аспект слева (до 48,
   если добавить вообще все) и по слою на каждый ненулевой первичный
   справа (максимум 6). Сверено с декомпилированным TileNodeRenderer.
   renderNode() — в игре так же: каждый аспект узла красит СВОЙ слой
   aspect.getColor() без подмешивания к другим (не усреднённый общий
   тон, как раньше пытались имитировать вручную здесь). background-color
   у каждого слоя — инлайн, задаёт JS; ЗДЕСЬ ниже только дефолтный серый
   (нейтральный, для пустого узла до первого добавления аспекта — JS
   тогда создаёт один слой вовсе без inline-цвета, читается вот этот
   CSS-дефолт).

   В игре слои аспектов складываются АДДИТИВНО поверх тёмного игрового
   фона (пещера/небо) — на сайте, где карточка светлая (см. историю у
   .cv-calc выше), аддитивное сложение выжгло бы всё в белый (та же
   причина, по которой ещё в Round #106 screen заменили на multiply —
   см. историю там). multiply здесь остался, но со МНОГИМИ слоями
   (не двумя) у него другая проблема — умножающее наложение накопительно
   темнеет с каждым слоем, и 10+ слоёв подряд свели бы узел почти к
   чёрному пятну независимо от цветов. Поэтому JS занижает opacity
   каждого слоя по мере роста их числа (см. cvRenderNodeLayers()) — при
   1-2 слоях она обычная (1), дальше плавно снижается, но не ниже .28,
   чтобы даже при полном наборе из 48 аспектов узел не тонул в черноте, а
   каждый цвет оставался различим.

   animation-delay у каждого слоя — свой, вычисляет и расставляет JS
   равномерно по циклу пульса (доля от 1.6s на число слоёв), чтобы слои
   не пульсировали синхронно одним пятном, а мерцали вразнобой — раньше
   для двух слоёв это была одна фиксированная величина (-.8s, половина
   цикла), теперь то же самое, но под любое количество слоёв.

   mask-position вместо background-position — сама заливка теперь фон
   (background-color), а кадр спрайта только маскирует форму (mask-
   image), поэтому и анимируется именно mask-position; steps(32) не
   интерполирует между кадрами, а просто переключает — так спрайт-листы
   всегда и анимируют. */
.cv-node-visual{
  position:absolute;
  top:0; left:0;
  width:100%; height:100%;
  background-color:#c3ccdb; /* дефолт для пустого узла — JS перекрывает inline-стилем у каждого реального слоя */
  transition:background-color .4s ease;
  mask-image:url('/images/nodes/node-glow.png');
  -webkit-mask-image:url('/images/nodes/node-glow.png');
  mask-repeat:no-repeat; -webkit-mask-repeat:no-repeat;
  mask-size:3072px 96px; -webkit-mask-size:3072px 96px; /* 32 × 96px */
  mix-blend-mode:multiply;
  animation:cvNodePulse 1.6s steps(32) infinite;
}
@keyframes cvNodePulse{
  from{mask-position:0 0; -webkit-mask-position:0 0;}
  to{mask-position:-3072px 0; -webkit-mask-position:-3072px 0;}
}
@media (prefers-reduced-motion: reduce){
  .cv-node-visual{animation:none; mask-position:-1536px 0; -webkit-mask-position:-1536px 0;}
}

/* Раунд #113: акцент "типа" узла (NodeType в терминах мода — свойство
   ОТДЕЛЬНОЕ от NodeModifier/кнопок Увядающий-Бледный-Обычный-Яркий,
   калькулятор его вообще не выбирает и не хранит). В игре
   (TileNodeRenderer.renderNode(), декомпилировано в этом же раунде)
   поверх слоёв аспектов ВСЕГДА рисуется ещё один маленький слой,
   выбранный по типу — NORMAL/UNSTABLE/DARK/TAINTED/HUNGRY/PURE, своя
   строка спрайта и цвет-обводка на каждый; раз выбора типа в
   калькуляторе нет, воспроизведён только NORMAL — вторая строка/"strip
   1" того же листа misc/nodes.png (маленькая тусклая искра), сохранена
   отдельным файлом public/images/nodes/node-type-normal.png. В игре этот
   слой подкрашен в маджента (RGB 255,0,255) с аддитивным наложением —
   на белой карточке аддитивное так же выжигало бы в белый (та же
   причина, что и у .cv-node-visual выше), поэтому здесь обычное
   наложение (без mix-blend-mode) и заниженная непрозрачность — читается
   как лёгкий розовато-сиреневый блик поверх шара, а не отдельное цветное
   пятно.

   Меньше самого узла (64px против 96px .cv-node-stage) и отцентрирован
   — в игре этот слой всегда заметно мельче основных слоёв аспектов.
   Статический элемент в разметке (не создаётся JS, в отличие от
   .cv-node-visual) — тип узла не меняется данными, он один и тот же
   всегда, и есть у ОБОИХ узлов (левого и правого), не только у
   энергетизированного справа. Стоит ПОСЛЕДНИМ внутри .cv-node-core (JS
   у cvRenderNodeLayers() вставляет новые .cv-node-visual ПЕРЕД ним, а не
   после — см. её комментарий в app.js), поэтому он всегда поверх слоёв
   аспектов независимо от их количества. */
.cv-node-type-accent{
  position:absolute;
  top:16px; left:16px;
  width:64px; height:64px;
  background-color:#ff8ef2;
  opacity:.5;
  mask-image:url('/images/nodes/node-type-normal.png');
  -webkit-mask-image:url('/images/nodes/node-type-normal.png');
  mask-repeat:no-repeat; -webkit-mask-repeat:no-repeat;
  mask-size:2048px 64px; -webkit-mask-size:2048px 64px; /* 32 × 64px */
  animation:cvNodeTypeShimmer 2.4s steps(32) infinite;
}
@keyframes cvNodeTypeShimmer{
  from{mask-position:0 0; -webkit-mask-position:0 0;}
  to{mask-position:-2048px 0; -webkit-mask-position:-2048px 0;}
}
@media (prefers-reduced-motion: reduce){
  .cv-node-type-accent{animation:none; mask-position:-1024px 0; -webkit-mask-position:-1024px 0;}
}

/* Раунд #110: кольцо пробуждённого узла — новый ассет, присланный
   пользователем (16-кадровый спрайт-лист, тогда называл его "просто
   такой ассет"), просьба «для права узла наложить ещё такой ассет» —
   ТОЛЬКО у правого/итогового узла (.cv-col-output), у левого/входного
   его нет. Поэтому это не общее правило по классу узла (как
   .cv-node-visual), а отдельный элемент .cv-node-energized-ring, который
   есть в разметке только внутри правого .cv-node-stage (см.
   centivis-calculator.blade.php) — сам класс просто ничего не даёт,
   если такого элемента нет в дереве.

   Раунд #113: выяснилось (пользователь прислал сам мод, jar) — ассет
   называется public/images/items/lightningringv.png, "кольцо" в игре
   рисуется только у ЭНЕРГЕТИЗИРОВАННОГО/"пробуждённого" узла
   (TileNodeEnergizedRenderer.func_147500_a(), декомпилировано в этом
   раунде) — поверх обычного рендера узла (тот же TileNodeRenderer.
   renderNode(), что и у любого узла), полностью своим цветом, БЕЗ
   перекраски (в игре — UtilsFX.renderFacingQuad(..., 0xFFFFFF), то есть
   белый/нейтральный множитель = ассет как есть). Ровно то же самое уже
   было устроено здесь и раньше (background-image без mask+background-
   color, см. следующий абзац) — механика, выбранная по наитию в Round
   #110, оказалась верной и без сверки с игрой; сам файл сохранён заново
   под точным именем node-energized-ring.png (побайтово тот же — сверено
   с оригиналом lightningringv.png из jar), .cv-node-frost переименован в
   .cv-node-energized-ring по всему файлу и в разметке.

   В отличие от .cv-node-visual, здесь НЕ mask-image + background-color,
   а обычный background-image: сам ассет уже цветной, перекрашивать его
   под цвет аспектов незачем (и в игре не перекрашивается, см. выше) —
   нужно именно наложить ассет как есть, своим собственным цветом.

   Отцентрирован, крупнее самого узла — лучи расходятся из центра и
   должны выходить за пределы шарика узла, а не обрезаться по его
   границе.

   Раунд #111: пользователь — «сделай этот ассет поверх остальных, а то
   он позади где-то» (в Round #110 кольцо было позади узла, z-index:0
   против z-index:1 у .cv-node-core — часть лучей закрывалась самим
   шариком узла). Теперь z-index:2 — выше .cv-node-core (z-index:1),
   кольцо целиком поверх узла независимо от порядка тегов в разметке
   (сама разметка тоже приведена в соответствие — идёт ПОСЛЕ
   .cv-node-core, см. centivis-calculator.blade.php — но именно z-index,
   а не порядок тегов, задаёт итоговый стек, на случай будущих правок
   разметки).

   Раунд #112: пользователь — «Увеличь скорость ассета и сделай размер
   поменьше». Размер 176px→140px (пропорция к узлу заодно чуть меньше —
   было почти вдвое больше 96px, теперь ближе к аккуратной надбавке).
   Анимация 3.2s→1.4s (была вдвое МЕДЛЕННЕЕ пульса узла — 1.6s, теперь
   быстрее его). background-size/keyframes/точка заморозки для
   prefers-reduced-motion пересчитаны под новый размер кадра (16×140 =
   2240px, середина — 1120px, та же логика, что и раньше при 176px). */
.cv-node-energized-ring{
  position:absolute;
  top:50%; left:50%;
  width:140px; height:140px;
  transform:translate(-50%,-50%);
  background-image:url('/images/nodes/node-energized-ring.png');
  background-repeat:no-repeat;
  background-size:2240px 140px; /* 16 × 140px */
  pointer-events:none;
  z-index:2;
  animation:cvRingDrift 1.4s steps(16) infinite;
}
@keyframes cvRingDrift{
  from{background-position:0 0;}
  to{background-position:-2240px 0;}
}
@media (prefers-reduced-motion: reduce){
  .cv-node-energized-ring{animation:none; background-position:-1120px 0;}
}

.cv-arrow{
  display:flex;
  align-items:center;
  justify-content:center;
  align-self:center;
  grid-row:1 / -1; /* через все ряды .cv-body (сколько бы их ни было — -1 это "последняя линия") — иначе align-self:center центрировал бы только внутри ряда 1 */
  width:34px; height:34px;
  color:var(--navy-soft);
  margin-top:30px;
}
.cv-arrow svg{width:100%; height:100%;}

/* Обёртка вокруг заглушки "пока не добавлено" + HUD чипов — сам список
   аспектов, БЕЗ строки выбора (та с Round #117 в своём отдельном ряду 2,
   см. .cv-add-row выше и комментарий у .cv-body). Один grid-ряд (3, см.
   .cv-body выше), чтобы у правой колонки (там перед узлом только HUD
   результата) было ровно то же место ряда и узлы совпадали по высоте.
   Сама по себе не grid и без своего оформления — просто группирует эти
   два элемента в один subgrid-ряд, внутри стек обычный блочный
   (margin-bottom у каждого элемента, как и раньше). */
/* Round #116: align-self:end — без этого (.cv-body{align-items:start} —
   общее для всех рядов грида) обе ячейки ряда 3 прижаты к ВЕРХНЕЙ границе
   своей строки, а высота самой строки — общая, подгоняется под более
   высокий вариант (то список чипов слева, то результат справа — зависит
   от конкретного набора аспектов). Пока в Round #115 у .cv-hud (справа)
   была ещё и stretch-растяжка между рядами бейджей (align-content:normal
   у многострочного flex по умолчанию), вся эта разница в высоте
   пряталась МЕЖДУ рядами бейджей результата — после того как Round #115
   её убрал (align-content:flex-start), она никуда не делась, просто
   целиком переехала ПОД последний ряд бейджей, перед .cv-node-stage
   (жалоба пользователя вторым скриншотом — тот же по сути лишний отступ,
   просто в другом месте). align-self:end прижимает всю ячейку ряда 3 к
   НИЖНЕЙ границе строки вместо верхней — тогда отступ перед .cv-node-
   stage снова всегда только margin-bottom самой .cv-hud/.cv-col-pre-node
   (10px/через margin у дочерних элементов), как и в более короткой
   колонке, а вся лишняя высота (если она есть) уходит НАД группой, между
   меткой ряда 1 (точнее — строкой выбора аспекта ряда 2 у левой колонки)
   и самими аспектами — там она не создаёт видимого "разрыва" рядом с
   узлом.

   Round #117: раньше .cv-col-pre-node включал в себя и .cv-add-row —
   тогда этот align-self:end двигал вниз ВЕСЬ блок целиком, включая
   строку выбора аспекта, стоило только справа оказаться больше рядов
   бейджей, чем слева (жалоба пользователя: "смещается элемент вниз") —
   строка выбора отсюда вынесена в свой ряд 2 именно поэтому, align-self
   здесь теперь двигает только список уже добавленных аспектов, а не
   элемент управления над ним. */
.cv-col-pre-node{grid-row:3; align-self:end;}

/* Строка выбора аспекта для добавления — обычный <select> с двумя
   optgroup (Первичные/Составные, см. centivis-calculator.blade.php) +
   кнопка «Добавить». Список сам по себе не цветной (цвет только у уже
   добавленных чипов ниже) — тут это просто текстовый выбор из 48. */
.cv-add-row{display:flex; gap:8px; margin-bottom:14px; grid-row:2; /* Round #117, см. .cv-body выше — свой ряд, не двигается вместе со списком аспектов */}
/* Раунд #115: сам <select data-cv-aspect-select> теперь hidden — источник
   структуры/значения для .cv-aspect-picker ниже (см. подробный комментарий
   у разметки в centivis-calculator.blade.php и у cvBuildAspectPicker() в
   app.js), в потоке верстки больше не участвует, поэтому здесь остались
   только базовые свойства значений на случай, если он всё же станет видим
   (например, при отключённом JS — тогда .cv-aspect-picker пуст и бесполезен,
   а обычный select всё ещё можно раскрыть и выбрать из него мышью/тачем). */
.cv-aspect-select{
  flex:1;
  min-width:0;
  font:inherit;
  font-size:12.5px;
  font-weight:600;
  padding:9px 10px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:var(--bg2);
  color:var(--navy);
}
.cv-aspect-select:focus{outline:none; border-color:var(--blue); box-shadow:0 0 0 3px rgba(47,141,255,.15);}
/* Раунд #115: .cv-aspect-picker — свой выпадающий список выбора аспекта
   вместо обычного <select> (тот прячется рядом, см. правило выше и
   комментарий в Blade) — единственный способ показать иконку аспекта
   внутри пункта списка, нативный <option> с картинкой не поддерживается
   ни одним массовым браузером. Кнопка (.cv-aspect-picker-btn) визуально —
   тот же стиль, что был у самого select (рамка/фон/радиус/паддинг), чтобы
   замена была незаметна по месту в макете. */
.cv-aspect-picker{position:relative; flex:1; min-width:0;}
.cv-aspect-picker-btn{
  display:flex;
  align-items:center;
  gap:8px;
  width:100%;
  font:inherit;
  font-size:12.5px;
  font-weight:600;
  padding:9px 10px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:var(--bg2);
  color:var(--navy);
  text-align:left;
  cursor:pointer;
  transition:border-color .2s, box-shadow .2s;
}
.cv-aspect-picker-btn:hover{border-color:var(--blue-light);}
.cv-aspect-picker-btn:focus, .cv-aspect-picker-btn[aria-expanded="true"]{
  outline:none;
  border-color:var(--blue);
  box-shadow:0 0 0 3px rgba(47,141,255,.15);
}
.cv-aspect-picker-icon{
  flex:0 0 auto;
  width:20px; height:20px;
  mask-repeat:no-repeat; mask-position:center; mask-size:contain;
  -webkit-mask-repeat:no-repeat; -webkit-mask-position:center; -webkit-mask-size:contain;
}
/* Пункт-заглушка ("Выберите аспект…") — своего цвета/иконки ещё нет,
   элемент просто убирается из раскладки, а не остаётся пустым квадратом. */
.cv-aspect-picker-icon-empty{display:none;}
.cv-aspect-picker-label{
  flex:1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
/* Сам список — обычный поповер поверх остального контента карточки,
   z-index:12 достаточно в пределах самой карточки (у неё нет собственного
   более высокого контекста), но заведомо ниже шапки/модалок сайта (те на
   50+, см. остальные z-index в файле). max-height + overflow — аспектов
   всего 48, список на весь список без ограничения не поместился бы на
   маленьких экранах. */
.cv-aspect-picker-list{
  position:absolute;
  top:calc(100% + 6px);
  left:0;
  right:0;
  z-index:12;
  max-height:280px;
  overflow-y:auto;
  padding:6px;
  border-radius:10px;
  border:1.5px solid var(--line);
  background:#fff;
  box-shadow:0 12px 28px rgba(15,23,42,.18);
}
.cv-aspect-picker-group{
  padding:8px 8px 4px;
  font-size:11px;
  font-weight:700;
  letter-spacing:.3px;
  text-transform:uppercase;
  color:var(--navy-soft);
}
.cv-aspect-picker-option{
  display:flex;
  align-items:center;
  gap:8px;
  width:100%;
  font:inherit;
  font-size:12.5px;
  font-weight:600;
  padding:7px 8px;
  border:none;
  border-radius:8px;
  background:none;
  color:var(--navy);
  text-align:left;
  cursor:pointer;
}
.cv-aspect-picker-option:hover, .cv-aspect-picker-option:focus{
  outline:none;
  background:rgba(47,141,255,.10);
}
.cv-aspect-picker-option .cv-aspect-picker-icon{width:22px; height:22px;}
.cv-aspect-picker-name{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* Раньше — плоский полупрозрачный синий чип, единственная кнопка в
   карточке без веса. Остальные основные кнопки сайта (.btn-login,
   .btn-connect) — сплошной синий градиент с приподнимающей синей тенью,
   так эта кнопка (единственное "основное" действие в калькуляторе —
   добавить аспект) теперь оформлена тем же приёмом, а не отдельным
   более скромным стилем. */
.cv-add-btn{
  flex:0 0 auto;
  font:inherit;
  font-size:12.5px;
  font-weight:700;
  padding:9px 16px;
  border-radius:10px;
  border:none;
  background:linear-gradient(135deg,#3f9bff,#1c6fe0);
  color:#fff;
  cursor:pointer;
  box-shadow:0 8px 18px rgba(28,111,224,.35);
  transition:background .2s, box-shadow .2s, transform .2s ease;
}
.cv-add-btn:hover{
  background:linear-gradient(135deg,#5aacff,#2f7ae5);
  box-shadow:0 10px 22px rgba(28,111,224,.45);
  transform:translateY(-1px);
}

.cv-chips-empty{
  margin:0 0 12px;
  padding:14px 12px;
  border-radius:10px;
  border:1.5px dashed var(--line);
  font-size:12.5px;
  font-weight:600;
  color:var(--navy-soft);
  text-align:center;
}

/* HUD-кластер аспектов — по референсу пользователя (скриншот в игре:
   иконка + число под ней, плотной группой) — заменяет и список чипов
   слева (был построчный, с именем и полем ввода), и ряд бейджей
   результата справа (был отдельным блоком над узлом). Не больше 3 в
   ряд по горизонтали — остальные переносятся на следующий ряд
   (flex-wrap с явным width под ровно 3 колонки, см. ниже почему width,
   а не max-width).

   ТРЕТЬЯ версия позиционирования. Было: маленькие иконки в тёмной
   панели в углу узла (жалоба: "вообще плохо получилось", панель
   закрывала узел) → крупные иконки по центру, наложенные НА узел через
   position:absolute с отрицательным top (жалоба: "они просто поверх
   узлов, [нужно] строго над узлами"). Теперь — обычный элемент в
   потоке документа, ПЕРЕД .cv-node-stage, а не поверх него: никакого
   position:absolute/top/z-index, просто нормальный блок с отступом
   снизу (margin-bottom), после которого дальше по потоку идёт сама
   модель узла. Это одновременно и единственный по-настоящему надёжный
   способ добиться "строго над, без наложения" (в отличие от
   вычисления конкретного top под конкретную высоту HUD, как было —
   такой расчёт слишком легко разъезжается при любом изменении
   размера/количества строк, что и произошло: см. историю
   .cv-result-total ниже, тот 46-пиксельный нахлёст был прямым
   следствием именно абсолютного позиционирования).

   Иконки немного уменьшены по просьбе пользователя (56px, было 72px).

   Подложки под HUD нет (см. Round #101/#102) — читаемость даёт
   filter:drop-shadow у иконки и text-shadow у числа.

   width задан ЯВНО (а не max-width) — при max-width на flex-контейнере
   без ограничения он по умолчанию растягивается на всю ширину колонки
   и не переносит по 3 в ряд, пока контент не займёт всю её ширину;
   явный width под ровно 3 колонки (3×56 + 2×8 = 184) и margin:0 auto
   центрируют группу без вычисления доступной ширины откуда-либо ещё.

   :empty — на входной стороне, пока не добавлено ни одного аспекта,
   группа пустая и не рисуется вовсе (и не съедает отступ margin-bottom,
   т.к. display:none). На стороне результата группа сама по себе не
   бывает пустой (шесть первичных всегда отрендерены сервером), но
   Round #105: отдельные бейджи внутри неё теперь прячутся — просьба
   пользователя "если справа у узла 0 аспектов, то скрывать аспект".
   Каждый бейдж получает нативный атрибут [hidden] (Blade — по
   умолчанию, до первого расчёта; calcCentivis() в app.js — дальше, по
   факту пересчитанного значения), см. правило .cv-hud-badge[hidden]
   ниже: без него [hidden] был бы молча перебит .cv-hud-badge{display:
   flex} (одинаковая специфичность, но правило класса идёт в файле
   позже правила атрибута). */
.cv-hud{
  width:184px;
  margin:0 auto 10px;
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  align-content:flex-start; /* Round #115: без этого — по умолчанию align-content:normal
    у многострочного flex-контейнера ведёт себя как stretch, а у .cv-hud
    справа (data-cv-result) высота задана не своим содержимым, а высотой
    ряда subgrid-а .cv-body, которая подгоняется под САМЫЙ высокий
    вариант между колонками (см. комментарий у .cv-body ниже). Из-за
    stretch вся эта лишняя высота распределялась МЕЖДУ строками бейджей
    результата, раздувая видимый отступ между ними в разы против отступа
    слева (где .cv-hud вне грида и высоту берёт по своему содержимому,
    stretch не влияет) — ровно то, на что пожаловался пользователь первым
    скриншотом с обведённым промежутком. flex-start прижимает строки
    бейджей друг к другу (отступ между ними — снова только gap ниже).

    Round #116: сам по себе flex-start не убирал лишнюю высоту ряда, а
    просто переносил её вниз группы, перед .cv-node-stage — предыдущая
    версия этого комментария называла это "так и задумано", но пользователь
    прислал ВТОРОЙ скриншот с тем же по сути отступом, просто в новом
    месте. Настоящее решение — align-self:end ниже: он прижимает всю
    ячейку .cv-hud целиком к НИЖНЕЙ границе строки вместо верхней
    (.cv-body{align-items:start} даёт верх по умолчанию для всех рядов),
    тогда лишняя высота уходит НАД группой бейджей, а перед .cv-node-
    stage остаётся только свой margin-bottom — тот же небольшой отступ,
    что и у более короткой колонки.

    Round #117: у левой колонки строка выбора аспекта (.cv-add-row)
    переехала в свой отдельный ряд 2 (раньше жила в том же ряду, что и
    список чипов, из-за чего align-self:end ниже двигал вниз и её тоже —
    см. подробности у .cv-col-pre-node) — с тех пор высоту ряда 3
    (список чипов слева vs результат справа) может задавать ЛЮБАЯ из
    двух колонок в зависимости от конкретного набора аспектов, не всегда
    левая, как предполагалось раньше.
  */
  gap:8px;
  grid-row:3; /* Round #117 (было 2) — правая колонка прямой grid/subgrid-ребёнок .cv-col-output, см. .cv-body; слева .cv-hud лежит внутри .cv-col-pre-node (не grid), там это просто игнорируется */
  align-self:end; /* Round #116, см. комментарий у align-content выше — правило симметрично на обе колонки, а не завязано на то, какая из них выше */
}
.cv-hud:empty{display:none; margin:0;}
.cv-hud-badge{
  position:relative;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:2px;
  width:56px;
}
.cv-hud-badge[hidden]{display:none;}
/* Раунд #106: карточка снова без тёмной подложки нигде (см. .cv-calc/
   .cv-node-stage выше) — просьба пользователя "просто тень... для
   аспектов" вместо любого фона. Drop-shadow обводит именно alpha-маску
   иконки, а не её заливку, так что даже у совсем светлого/белого
   аспекта появляется тёмная тень независимо от его собственного цвета.

   Раунд #107: первая версия тени (симметричный мягкий блюр по контуру)
   на реальном скриншоте пользователя оказалась не видна вовсе — те же
   иконки аспектов уже сами по себе перистые/светящиеся по краю (мягкая
   alpha-маска, не жёсткий силуэт), и симметричная тень того же порядка
   размытия просто тонула в этом собственном ореоле иконки. Заменено на
   тень СО СМЕЩЕНИЕМ вниз, темнее и с меньшим блюром — она ложится
   отдельным тёмным пятном под иконкой, а не растворяется в её крае.

   Раунд #108: даже смещённая тень терялась на самых бледных аспектах
   (Aer и подобные, почти белые) — сама заливка там уже настолько близка
   к фону карточки, что даже заметная тень снизу не даёт иконке "цельно"
   читаться как объект. Добавлена вторая, ПЛОТНАЯ И ПОЧТИ БЕЗ РАЗМЫТИЯ
   тень без смещения (0 0 1px, но заметной непрозрачности) — она ложится
   вплотную по всему контуру силуэта, как тонкая тёмная обводка, а не
   мягкий ореол: обводка не зависит от собственного цвета заливки и
   поэтому одинаково хорошо очерчивает что тёмный, что совсем светлый
   аспект.

   Раунд #109 — НАЙДЕНА РЕАЛЬНАЯ ПРИЧИНА, почему тень была не видна на
   присланном пользователем скриншоте: filter:drop-shadow и mask-image
   были заданы на одном и том же элементе (.cv-hud-icon), а в этой
   комбинации браузер (проверено на реальном движке) тень не рисует
   ВООБЩЕ — не "тускло", а нулевым результатом, при любых значениях
   blur/opacity. Изолированная проверка: обычный div без маски + такой
   же filter — тень рисуется; тот же замаскированный div, но filter
   перенесён на обёртку СНАРУЖИ (а маска осталась на внутреннем
   элементе) — тень тоже рисуется, причём именно так, как ожидалось с
   самого начала. Поэтому теперь тень и маска разведены по разным
   элементам: .cv-hud-icon-shadow — просто обёртка нужного размера,
   держит filter, ничего не красит и не маскирует сама; .cv-hud-icon
   внутри неё (100% ширины/высоты) — как раньше, но БЕЗ filter, только
   mask-image + background-color (задаются в разметке, Blade/app.js —
   см. cvAspectIconStyle() и cvBuildChip() в app.js, разметка в
   centivis-calculator.blade.php). Значения тени заодно усилены (третья
   тень добавлена, opacity выше) — раз сам механизм подтверждён рабочим,
   нет смысла останавливаться на прежних, уже признанных недостаточными
   величинах. */
.cv-hud-icon-shadow{
  display:block;
  width:56px; height:56px;
  filter:drop-shadow(0 0 1px rgba(15,23,42,.8)) drop-shadow(0 0 2.5px rgba(15,23,42,.55)) drop-shadow(0 3px 5px rgba(15,23,42,.5));
}
.cv-hud-icon{
  display:block;
  width:100%; height:100%;
  mask-repeat:no-repeat; mask-position:center; mask-size:contain;
  -webkit-mask-repeat:no-repeat; -webkit-mask-position:center; -webkit-mask-size:contain;
}
/* И читаемое число результата (span), и редактируемое количество
   входного аспекта (number-input) — одна и та же вёрстка/размер, просто
   у инпута дополнительно убран нативный вид числового поля (обводка,
   спиннеры), чтобы он не выделялся среди read-only чисел справа.

   Раунд #106: текст снова тёмный (var(--navy)), а не белый — карточка
   без тёмной подложки, белый текст на белом/светлом фоне был бы
   невидим. Тень лёгкая синеватая для глубины (тот же приём, что и у
   .cv-hud-icon выше), а не тяжёлая чёрная — той требовался контраст с
   тёмным фоном узла, здесь фон уже сам по себе светлый.

   Раунд #105: шрифт — var(--font-minecraft) (см. @font-face в начале
   этого раздела), присланный пользователем файл. font-weight здесь
   намеренно normal, а не унаследованный 800 — в файле только один
   начертание (Regular), жирным браузер его не утолщает, а синтетически
   "утолщает" пиксели смазыванием (fake bold), что для пиксельного
   шрифта выглядит грязно; вес компенсирован чуть большим размером и
   letter-spacing вместо этого. */
.cv-hud-value{
  width:100%;
  border:none;
  background:transparent;
  padding:0;
  margin:0;
  font:inherit;
  font-family:var(--font-minecraft);
  font-weight:400;
  font-size:18px;
  letter-spacing:.5px;
  line-height:1.2;
  text-align:center;
  color:var(--navy);
  text-shadow:0 1px 2px rgba(31,90,180,.18);
  -moz-appearance:textfield;
}
.cv-hud-value::-webkit-outer-spin-button,
.cv-hud-value::-webkit-inner-spin-button{-webkit-appearance:none; margin:0;}
.cv-hud-value:focus{outline:none; background:rgba(47,141,255,.12); border-radius:4px; text-shadow:none;}
/* Крестик-удаление — скрыт по умолчанию (сама HUD-иконка не должна
   выглядеть как форма), показывается по наведению/фокусу поля
   количества (:focus-within — так же срабатывает и с тача при тапе по
   числу, а не только у мыши на hover). */
.cv-hud-remove{
  position:absolute;
  top:-5px; right:-5px;
  width:18px; height:18px;
  border-radius:50%;
  border:none;
  background:#d64545;
  color:#fff;
  font-size:12px;
  line-height:1;
  display:flex;
  align-items:center;
  justify-content:center;
  cursor:pointer;
  opacity:0;
  transition:opacity .15s;
  box-shadow:0 1px 3px rgba(0,0,0,.6);
}
.cv-hud-badge:hover .cv-hud-remove,
.cv-hud-badge:focus-within .cv-hud-remove{opacity:1;}
.cv-hud-remove:hover{background:#ff6b6b;}

/* "Итого: N cV/тик" — под моделью узла, прижата к правому краю колонки
   (просьба пользователя: "перенести вправо вниз"). margin-top снова
   обычный (14px) — раньше (Round #102) HUD накладывалась на узел через
   position:absolute и своей высотой заезжала НИЖЕ границы .cv-node-
   stage, из-за чего эта строка наезжала на второй ряд бейджей и
   margin-top пришлось раздувать до 60px, чтобы компенсировать. Теперь
   HUD — обычный элемент в потоке ПЕРЕД узлом (см. .cv-hud выше), ничего
   не накладывается и не выступает за пределы узла снизу — компенсация
   больше не нужна. */
.cv-result-total{
  grid-row:5; /* Round #117 (было 4), см. .cv-body */
  margin:14px 0 0;
  text-align:right;
  font-size:13.5px;
  color:var(--navy-soft);
}
.cv-result-total strong{font-size:16px; color:var(--navy);}

@media (max-width: 720px){
  /* Раунд #106: на мобильных колонки складываются в столбец — выравнивание
     .cv-node-stage по рядам (см. .cv-body выше) тут не нужно и мешало бы
     (subgrid на 4 явных ряда при одной колонке пытался бы впихнуть все три
     верхнеуровневых элемента — .cv-col-input/.cv-arrow/.cv-col-output — в
     эти же 4 ряда друг поверх друга). Явные ряды/subgrid сброшены обратно
     на простое блочное течение — .cv-col-input целиком, потом стрелка,
     потом .cv-col-output целиком, как и было до Round #106. */
  .cv-body{grid-template-columns:1fr; grid-template-rows:none;}
  .cv-col-input, .cv-col-output{display:block; grid-row:auto;}
  .cv-arrow{margin:4px auto; transform:rotate(90deg); grid-row:auto;}
}
@media (max-width: 420px){
  .cv-calc{
    padding:22px 18px 26px;
    /* Round #118: та же текстура рун, но меньше и ближе к углу — на
       узкой карточке фиксированные 320px из десктопного правила выше
       перекрыли бы половину заголовка/текста, а не легли фоновым узором. */
    background-size:200px 200px;
    background-position:right -22px top -22px;
  }
  .cv-add-row{flex-direction:column;}
}
