
  /* Generated by scripts/fetch_fonts.py — do not edit by hand. */
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-400-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-400-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-400-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/manrope-300-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/manrope-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/manrope-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(/static/fonts/manrope-800-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-200-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-200-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-200-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-300-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-300-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-300-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-400-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-500-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-500-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-600-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-600-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* arabic */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-700-arabic.woff2) format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* latin-ext */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Vazirmatn';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/vazirmatn-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

  /*
    فونت رخ (Rokh) یک فونت تجاری/پولیه و روی هیچ CDN عمومی‌ای در دسترس نیست.
    اگه لایسنسش رو از fontiran.com خریدی، فایل‌های وب‌فونت (woff2/woff) رو با همین اسم‌ها
    داخل پوشه‌ی static/fonts/ کنار portfolio_app.py بذار تا خودکار فعال بشه.
    این @font-face فقط وقتی رندر می‌شه که سرور واقعاً فایل‌ها رو پیدا کنه (چک در Python) —
    در غیر این صورت این بلوک و کلمه‌ی Rokh از استک فونت کاملاً حذف می‌شن تا مرورگر هرگز
    درخواست 404 برای فایل‌های نبودنی نفرسته.
  */
  
  :root{
    /* ---- fonts ---- */
    --font-fa:'Vazirmatn',sans-serif;
    --font-en:'Manrope','Vazirmatn',sans-serif;
    --font-mono:'IBM Plex Mono',monospace;

    /* ---- color system (kept close to previous palette; refined for depth) ---- */
    --color-bg:#0a0a0a;
    --color-surface:#131313;
    --color-surface-elevated:#1a1a1a;
    --color-text:#f4f2ee;
    --color-text-muted:#9a9894;
    --color-text-faint:#666461;
    --color-border:#272727;
    --color-accent:#c9a468;
    --color-accent-hover:#dab97e;

    /* legacy aliases so existing rules keep working untouched */
    --bg:var(--color-bg);
    --bg-elev:var(--color-surface);
    --line:var(--color-border);
    --ink:var(--color-text);
    --ink-dim:var(--color-text-muted);
    --ink-faint:var(--color-text-faint);
    --accent:var(--color-accent);
    --r:1px;

    /* ---- theme-dependent surfaces (dark defaults) ---- */
    --nav-scrolled-bg:rgba(11,11,11,.86);
    /* Nearly opaque on purpose: the panel hangs over the hero, and anything
       translucent enough to read through makes the links unreadable. */
    --nav-sub-bg:rgba(16,16,16,.97);
    --menu-bg:rgba(10,10,10,.98);
    --overlay-bg:rgba(8,8,8,.97);
    --overlay-bg-soft:rgba(8,8,8,.8);
    --ctrl-bg:rgba(10,10,10,.4);
    --ctrl-bg-hover:rgba(10,10,10,.7);
    --hero-scrim-mid:rgba(11,11,11,.22);
    --hero-scrim-top:rgba(11,11,11,.4);
    --progress-track:rgba(255,255,255,.22);
    --logo-mono-filter:brightness(0) invert(1);
    --rec-red:#e5484d;
    /* tinted pill for the phone-only "start a project" shortcut in the header.
       A pair of theme vars rather than color-mix(): the accent differs per theme
       and color-mix has no fallback in the older Safari/Firefox builds this site
       still gets traffic from. */
    --cta-mobile-bg:rgba(201,164,104,.13);
    --cta-mobile-border:rgba(201,164,104,.45);

    /* ---- global film texture (see GLOBAL FILM TEXTURE section) ---- */
    --grain-opacity:.58;
    --grain-blend:overlay;
    --grain-tile:150px;
    --vignette:inset 0 0 220px 70px rgba(0,0,0,.5);

    /* ---- layout ---- */
    --container-width:1360px;
    --container-pad:6vw;
    --section-space-desktop:150px;
    --section-space-mobile:88px;

    --radius-sm:8px;
    --radius-md:16px;

    --shadow-soft:0 20px 50px -24px rgba(0,0,0,.6);
    --transition-fast:150ms ease;
    --transition-normal:300ms ease;

    /* ---- central typography system ----
       every size below has a floor at/above the project's readability minimums
       (metadata ~12-13px, body copy 16px+, headings scale up from there).
       nothing site-wide should hard-code a raw rem/px size outside this list. */
    --text-2xs: clamp(0.8125rem, 0.8rem + 0.1vw, 0.875rem);   /* ~13-14px: metadata floor */
    --text-xs: clamp(0.8125rem, 0.8rem + 0.1vw, 0.875rem);    /* kept as alias — see --text-2xs */
    --text-sm: clamp(0.94rem, 0.9rem + 0.2vw, 1rem);          /* ~15-16px: labels, nav */
    --text-base: clamp(1rem, 0.96rem + 0.25vw, 1.125rem);     /* ~16-18px: body copy */
    --text-lg: clamp(1.12rem, 1.02rem + 0.45vw, 1.35rem);
    --text-xl: clamp(1.35rem, 1.15rem + 0.8vw, 1.8rem);

    --heading-card: clamp(1.25rem, 1.1rem + 0.5vw, 1.85rem);
    --heading-sm: clamp(1.7rem, 1.3rem + 1.4vw, 2.6rem);
    --heading-md: clamp(2.2rem, 1.5rem + 2.5vw, 4rem);
    --heading-lg: clamp(2.8rem, 1.8rem + 4vw, 6.5rem);
    /* spec-named aliases for the same scale, so both naming schemes work */
    --heading-section: var(--heading-md);
    --heading-page: var(--heading-lg);
    --heading-hero: var(--heading-lg);

    --line-height-tight: 1.2;
    --line-height-normal: 1.6;
    --line-height-relaxed: 1.85;
  }

  /* ---------- LIGHT (DAY) MODE — cream / nude palette ---------- */
  [data-theme="light"]{
    --color-bg:#f4ede2;
    --color-surface:#ede3d3;
    --color-surface-elevated:#e7dbc8;
    --color-text:#2b2520;
    --color-text-muted:#6e6255;
    --color-text-faint:#9a8b7a;
    --color-border:#ddcfba;
    --color-accent:#a0763a;
    --color-accent-hover:#875f2c;

    --nav-scrolled-bg:rgba(244,237,226,.88);
    --nav-sub-bg:rgba(248,243,235,.98);
    --menu-bg:rgba(244,237,226,.98);
    --overlay-bg:rgba(240,232,219,.97);
    --overlay-bg-soft:rgba(43,37,32,.45);
    --ctrl-bg:rgba(244,237,226,.55);
    --ctrl-bg-hover:rgba(244,237,226,.85);
    --hero-scrim-mid:rgba(244,237,226,.18);
    --hero-scrim-top:rgba(244,237,226,.3);
    --progress-track:rgba(43,37,32,.22);
    --logo-mono-filter:brightness(0);
    --cta-mobile-bg:rgba(160,118,58,.13);
    --cta-mobile-border:rgba(160,118,58,.42);
    --shadow-soft:0 20px 50px -24px rgba(90,70,40,.28);

    /* cream mode needs a much lighter hand: overlay blend blows out on a light
       ground, so soft-light + a warm (not black) vignette. */
    --grain-opacity:.3;
    --grain-blend:soft-light;
    --vignette:inset 0 0 240px 80px rgba(92,70,42,.16);
  }
  html{background:var(--color-bg);}

  *{box-sizing:border-box; margin:0; padding:0;}
  html{scroll-behavior:smooth; overflow-x:hidden;}
  body{
    background:var(--color-bg);
    color:var(--color-text);
    font-family:var(--font-fa), var(--font-en);
    font-size:var(--text-base);
    font-weight:400;
    line-height:var(--line-height-relaxed);
    overflow-x:hidden;
    -webkit-font-smoothing:antialiased;
  }
  [dir="ltr"] body{ font-family:var(--font-en), var(--font-fa); }

  /* ---------- GLOBAL FILM TEXTURE — grain + soft vignette ---------- */
  /* Two fixed, non-interactive layers over the whole public site: an SVG
     fractal-noise grain (::after, blended) and a soft edge vignette (::before).
     z-index 900 sits above page content and the sticky nav (500) but below the
     mobile menu (1200), lightbox (2000) and modals/toast (3000), so overlays
     and dialogs stay clean. Intensity lives in the --grain-* / --vignette
     tokens in :root (dark) and [data-theme="light"] — tune there, not here. */
  body::before, body::after{
    content:''; position:fixed; inset:0; pointer-events:none;
  }
  body::before{ z-index:900; box-shadow:var(--vignette); }
  body::after{
    /* 400, not 900 where the vignette sits: the grain has to stay BELOW the
       video cards so their covers can opt out of it (see .card, z-index:401) —
       excluding an element from a mix-blend-mode layer is only possible by
       painting it above that layer. Cards can't simply go above 900 instead,
       or they'd also clear the sticky nav (500) and ride over it on scroll.
       The nav is the only component between 400 and 900, so the bar's own
       pixels go ungrained; whatever shows through its translucent background
       is still textured. */
    z-index:400;
    opacity:var(--grain-opacity);
    mix-blend-mode:var(--grain-blend);
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='150' height='150'><filter id='filmgrain'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncA type='linear' slope='0.55'/></feComponentTransfer></filter><rect width='100%25' height='100%25' filter='url(%23filmgrain)'/></svg>");
    background-size:var(--grain-tile) var(--grain-tile);
  }
  /* the admin panel is a work surface, not a film frame — it opts out */
  body:has(#adminRoot.open)::before, body:has(#adminRoot.open)::after{ display:none; }
  @media print{ body::before, body::after{ display:none; } }

  /* ---- typography utility classes ---- */
  .eyebrow{
    font-family:var(--font-mono); font-size:var(--text-xs); letter-spacing:.18em; text-transform:uppercase;
    color:var(--color-accent); display:block;
  }
  /* Persian text can't take the Latin eyebrow treatment: IBM Plex Mono has no
     Persian glyphs (so it silently falls back mid-run), letter-spacing forces
     apart letters that are supposed to stay joined inside a word, and
     uppercase is a no-op. Same size and same colour — only the face, tracking
     and weight change, so the label still reads as an eyebrow. */
  [dir="rtl"] .eyebrow{
    font-family:var(--font-fa), sans-serif; letter-spacing:0; text-transform:none; font-weight:300;
  }
  ::selection{background:var(--accent); color:var(--bg);}
  a{color:inherit; text-decoration:none;}
  button{font-family:inherit; cursor:pointer; background:none; border:none; color:inherit;}
  img{max-width:100%; display:block;}
  :focus-visible{outline:1px solid var(--accent); outline-offset:4px;}

  .mono{font-family:'IBM Plex Mono', monospace; letter-spacing:.12em;}

  /* corner frame — quiet viewfinder motif, the one signature device */
  .frame{position:relative;}
  .frame::before, .frame::after, .frame .fc1, .frame .fc2{
    content:''; position:absolute; width:18px; height:18px; border-color:var(--accent); opacity:.55; pointer-events:none;
  }
  .frame::before{top:18px; inset-inline-start:18px; border-top:1px solid; border-inline-start:1px solid;}
  .frame::after{top:18px; inset-inline-end:18px; border-top:1px solid; border-inline-end:1px solid;}
  .frame .fc1{bottom:18px; inset-inline-start:18px; border-bottom:1px solid; border-inline-start:1px solid;}
  .frame .fc2{bottom:18px; inset-inline-end:18px; border-bottom:1px solid; border-inline-end:1px solid;}

  /* ---------- NAV ---------- */
  header.site-nav{
    position:fixed; top:0; left:0; right:0; z-index:500;
    display:flex; align-items:center; justify-content:space-between;
    padding-block:26px;
    padding-inline:max(var(--container-pad), calc((100% - var(--container-width)) / 2));
    transition:padding .4s, background .4s;
  }
  header.site-nav.scrolled{padding-block:16px; background:var(--nav-scrolled-bg); backdrop-filter:blur(10px); border-bottom:1px solid var(--line);}
  .brand{display:flex; align-items:center; gap:10px; font-weight:500; font-size:var(--text-base); letter-spacing:.01em; flex-shrink:0; color:var(--ink);}
  .brand img{height:var(--logo-h-desktop, 34px); width:auto; display:block; transition:height var(--transition-normal);}
  header.site-nav.scrolled .brand img{height:calc(var(--logo-h-desktop, 34px) * .82);}
  @media(max-width:700px){
    .brand img{height:var(--logo-h-mobile, 28px) !important;}
    /* The bar genuinely did not fit: logo + brand name (187px) plus the right
       hand controls (255px) needed 442px of a 330px row, which pushed the
       control block off-screen (measured left:-89px) and slid it under the
       logo. The logo already carries the identity here, and the socials are
       repeated in the mobile menu and the contact section, so both step out
       and the row drops to ~189px with room to spare. */
    /* !important because renderBrand() writes display inline on this element
       (it honours the admin's "show brand name" toggle), and an inline style
       would otherwise win — same reason the logo height above needs it. */
    .brand #navBrandText{display:none !important;}
    .nav-right #navSocial{display:none;}
    .nav-right{gap:14px;}
  }
  nav.links{display:flex; gap:30px; align-items:center;}
  nav.links a{
    font-family:var(--font-fa), var(--font-en); font-size:var(--text-sm); font-weight:400;
    letter-spacing:.01em; color:var(--ink-dim); position:relative; padding-bottom:4px; white-space:nowrap;
  }
  [dir="ltr"] nav.links a{ font-family:var(--font-en), var(--font-fa); }
  nav.links a::after{content:''; position:absolute; bottom:0; inset-inline-start:0; width:0; height:1px; background:var(--accent); transition:width .3s;}
  nav.links a:hover::after, nav.links a.active::after{width:100%;}
  nav.links a:hover, nav.links a.active{color:var(--ink);}
  nav.links a:focus-visible{outline:1px solid var(--accent); outline-offset:5px; border-radius:2px;}

  /* ---------- NAV DROPDOWN (service pages) ----------
     The header is position:fixed with no clipping, so the panel can simply
     hang off the trigger. It opens on hover and on :focus-within, so keyboard
     users reach it by tabbing rather than only with a pointer. The gap between
     the trigger and the panel is bridged by padding on the wrapper, otherwise
     the panel closes while the pointer crosses the empty space. */
  .nav-item{position:relative; display:flex; align-items:center;}
  .nav-item.has-sub{padding-bottom:14px; margin-bottom:-14px;}
  .nav-trigger{display:inline-flex; align-items:center; gap:6px;}
  .nav-trigger .chev{
    width:9px; height:9px; border-inline-end:1.4px solid currentColor; border-bottom:1.4px solid currentColor;
    transform:rotate(45deg) translate(-1px,-1px); transition:transform .3s; flex-shrink:0; opacity:.8;
  }
  .nav-item.has-sub:hover .nav-trigger .chev,
  .nav-item.has-sub:focus-within .nav-trigger .chev{transform:rotate(-135deg) translate(-2px,-2px);}
  .nav-sub{
    position:absolute; top:100%; inset-inline-start:50%; transform:translateX(50%) translateY(8px);
    min-width:268px; padding:10px; border-radius:16px;
    background:var(--nav-sub-bg, rgba(16,16,16,.97)); border:1px solid var(--line);
    box-shadow:0 24px 50px -22px rgba(0,0,0,.9);
    backdrop-filter:blur(14px);
    display:flex; flex-direction:column; gap:2px;
    opacity:0; visibility:hidden; pointer-events:none;
    transition:opacity .25s, transform .25s, visibility .25s;
  }
  [dir="ltr"] .nav-sub{transform:translateX(-50%) translateY(8px);}
  .nav-item.has-sub:hover .nav-sub,
  .nav-item.has-sub:focus-within .nav-sub{
    opacity:1; visibility:visible; pointer-events:auto; transform:translateX(50%) translateY(0);
  }
  [dir="ltr"] .nav-item.has-sub:hover .nav-sub,
  [dir="ltr"] .nav-item.has-sub:focus-within .nav-sub{transform:translateX(-50%) translateY(0);}
  nav.links .nav-sub a{
    display:flex; align-items:center; gap:11px;
    padding:11px 14px; border-radius:10px; white-space:normal; line-height:1.6;
    color:var(--ink-dim); transition:background .2s, color .2s; padding-bottom:11px;
  }
  nav.links .nav-sub a::after{display:none;}
  nav.links .nav-sub a:hover{background:var(--ctrl-bg-hover, rgba(255,255,255,.05)); color:var(--accent);}
  .nav-sub .ico, .mm-sub .ico{
    display:inline-flex; align-items:center; justify-content:center; flex-shrink:0;
    width:32px; height:32px; border-radius:9px; border:1px solid var(--line);
    color:var(--accent); transition:border-color .2s, background .2s;
  }
  .nav-sub .ico svg, .mm-sub .ico svg{width:17px; height:17px;}
  .nav-sub a:hover .ico{border-color:var(--accent); background:rgba(201,164,104,.1);}
  .nav-sub .all{
    margin-top:4px; padding-top:11px; border-top:1px solid var(--line);
    font-size:var(--text-2xs); color:var(--ink-faint) !important;
  }
  .nav-sub .all:hover{color:var(--accent) !important;}

  /* Mobile menu: no hover, so the sub-links are simply listed under the
     parent as an indented group.

     Every rule here is qualified with `.mobile-menu nav` on purpose. Plain
     `.mm-sub a` loses to `.mobile-menu nav a` on specificity, so the sub-links
     were inheriting the 1.6rem/500 of a top-level item — which made them the
     same weight as their own parent and ran the longest title off the edge of
     the screen. */
  .mobile-menu nav .mm-sub{
    display:flex; flex-direction:column; gap:2px; margin:8px auto 12px;
    width:min(304px, 80vw);
    padding-inline-start:14px; border-inline-start:1px solid var(--line);
  }
  .mobile-menu nav .mm-sub a{
    font-size:1rem; font-weight:300; letter-spacing:0; color:var(--ink-dim);
    display:flex; align-items:center; gap:11px; padding:8px 0; min-width:0;
  }
  .mobile-menu nav .mm-sub a span{
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .mobile-menu nav .mm-sub a.active{color:var(--accent);}
  .mm-sub .ico{width:29px; height:29px; border-radius:8px;}
  .mm-sub .ico svg{width:15px; height:15px;}
  .nav-right{display:flex; align-items:center; gap:20px;}
  .nav-cta{
    background:var(--accent); color:var(--bg); font-family:var(--font-fa), var(--font-en); font-size:var(--text-sm); letter-spacing:.01em;
    padding:10px 20px; border-radius:999px; font-weight:600; white-space:nowrap; transition:transform .25s, box-shadow .25s;
  }
  .nav-cta:hover{transform:translateY(-2px); box-shadow:0 10px 24px -10px rgba(201,164,104,.6);}

  /* ---- phone-only header shortcut to the enquiry form ----
     Most of the traffic is mobile, where the desktop .nav-cta is hidden and the
     only route to the form is two taps through the burger menu. This is that
     route in one tap, sitting in the header where the eye already lands.
     Styled as a quiet outlined pill rather than a second solid button so it
     doesn't compete with the logo, and carries the same REC dot as the hero's
     camera overlay — the shortcut reads as part of the film language, not as a
     bolted-on ad. Latin text in both languages by design (set from the admin). */
  .nav-cta-mobile{
    display:none; align-items:center; justify-content:center; gap:7px; flex:none;
    /* Slightly tighter on the dot side than the text side. The dot carries less
       visual weight than the cap-height letters beside it, so equal padding
       reads as if the label is pushed right; 10/14 puts the *label* on the
       pill's optical centre. direction:ltr makes left/right stable in RTL too. */
    padding:6px 14px 6px 10px; border-radius:999px;
    border:1px solid var(--cta-mobile-border); background:var(--cta-mobile-bg);
    color:var(--accent); font-family:var(--font-en), sans-serif;
    font-size:11px; font-weight:600; letter-spacing:.13em; text-transform:uppercase;
    white-space:nowrap; direction:ltr;
    transition:transform .25s ease, box-shadow .25s ease, background-color .25s ease;
  }
  .nav-cta-mobile:focus-visible{outline:1px solid var(--accent); outline-offset:3px;}
  .nav-cta-mobile .ncm-dot{
    width:6px; height:6px; border-radius:50%; background:var(--rec-red); flex:none;
    animation:ncmBlink 1.9s steps(1,end) infinite;
  }
  @keyframes ncmBlink{ 0%,55%{opacity:1;} 56%,100%{opacity:.25;} }
  @media (prefers-reduced-motion:reduce){ .nav-cta-mobile .ncm-dot{animation:none;} }
  /* (blanking the admin label hides the pill via an inline display:none from
     renderStatic — :empty can't be used here, the REC dot is always a child) */
  /* Shown exactly where the desktop CTA drops out (see the max-width:1080px
     block near the end of the stylesheet, which hides .nav-right .nav-cta).

     Centred on the HEADER, not inside .nav-right: as a flex item it sat wherever
     the controls around it left room, which put it visibly right of centre. It
     is taken out of flow and pinned to the bar's midpoint instead, so it reads
     as the one deliberate thing in the middle of the header. The header is
     position:fixed, which is the containing block; top:50% keeps it centred
     through the .scrolled padding change without a second rule. */
  @media(max-width:1080px){
    .nav-cta-mobile{
      display:inline-flex;
      position:absolute; left:50%; top:50%;
      transform:translate(-50%, -50%);
    }
    .nav-cta-mobile:active{transform:translate(-50%, -50%) scale(.96);}
  }
  /* Tightening the header row to make space for the pill.
     These live here, AFTER the base `.nav-right{gap:20px}` declaration, on
     purpose: the identical override up in the max-width:700px block sits
     *before* that base rule, so at equal specificity the base won and the
     narrow-screen gap was never actually applied. Measured on a 320px viewport
     the row needed 355px and pushed the burger to left:-16px — off-screen. */
  @media(max-width:700px){ .nav-right{gap:14px;} }
  /* On a 320px phone the row is genuinely tight: trim the pill, the gaps and
     the page gutter rather than let anything slide off the edge. Measured
     total after this: ~315px of 320. */
  @media(max-width:400px){
    .nav-cta-mobile{padding:5px 10px; gap:6px; font-size:10px; letter-spacing:.1em;}
    .nav-right{gap:10px;}
    header.site-nav{padding-inline:14px;}
  }

  .social-row{display:flex; align-items:center; gap:18px;}
  .social-row a{color:var(--ink-faint); display:flex; transition:color .25s, transform .25s;}
  .social-row a:hover{color:var(--accent); transform:translateY(-2px);}
  .social-row svg{width:16px; height:16px;}
  .lang-toggle{
    color:var(--ink-faint); font-family:'IBM Plex Mono'; font-size:var(--text-2xs); letter-spacing:.1em; display:flex; gap:8px;
  }
  /* (.theme-toggle removed with the light/dark switch — see SITE_THEME) */
  .lang-toggle span{transition:color .2s;}
  .lang-toggle span.active{color:var(--accent);}
  .burger{display:none; flex-direction:column; gap:5px; padding:6px; z-index:1201; position:relative;}
  .burger span{width:20px; height:1px; background:var(--ink); transition:transform .3s, opacity .3s;}
  .burger.open span:nth-child(1){transform:translateY(6px) rotate(45deg);}
  .burger.open span:nth-child(2){opacity:0;}
  .burger.open span:nth-child(3){transform:translateY(-6px) rotate(-45deg);}

  /* ---------- MOBILE FULLSCREEN MENU ---------- */
  .mobile-menu{
    position:fixed; inset:0; z-index:1200; background:var(--menu-bg); backdrop-filter:blur(14px);
    display:flex; flex-direction:column; justify-content:center; align-items:center; gap:26px;
    opacity:0; pointer-events:none; transform:translateY(-12px); transition:opacity .4s ease, transform .4s ease;
  }
  .mobile-menu.open{opacity:1; pointer-events:auto; transform:translateY(0);}
  .mobile-menu nav{display:flex; flex-direction:column; align-items:center; gap:22px;}
  .mobile-menu nav a{font-size:1.6rem; font-weight:500; color:var(--ink); letter-spacing:.01em;}
  .mobile-menu nav a.active{color:var(--accent);}
  .mobile-menu .nav-cta{margin-top:10px; padding:14px 32px; font-size:var(--text-2xs);}
  .mobile-menu .mm-info{margin-top:30px; text-align:center; font-family:'IBM Plex Mono'; font-size:var(--text-2xs); color:var(--ink-dim); display:flex; flex-direction:column; gap:8px;}
  .mobile-menu .social-row{justify-content:center; margin-top:16px;}
  @media(min-width:1081px){ .mobile-menu{display:none;} }

  /* ---------- HERO ---------- */
  .hero{
    position:relative; min-height:100svh; display:flex; flex-direction:column; justify-content:center;
    padding:0 6vw; margin:0; overflow:hidden;
  }

  /* ---- slides (crossfade + optional Ken Burns) ---- */
  /* NOTE: this layer intentionally uses z-index:0 (not negative). A negative
     z-index here previously let some ancestor's background paint over the
     slide images depending on stacking context, causing an intermittent
     all-black Hero. Positive, explicit stacking below fixes it for good. */
  .hero-slides{position:absolute; top:0; bottom:0; inset-inline-start:0; inset-inline-end:0; z-index:0; overflow:hidden; background:var(--bg);}
  .hero-slide{position:absolute; inset:0; opacity:0; transition:opacity 1.1s ease; z-index:0;}
  .hero-slide.active{opacity:1; z-index:1;}
  .hero-slide picture{display:block; width:100%; height:100%;}
  .hero-slide img, .hero-slide video{
    width:100%; height:100%; object-fit:cover;
    filter:grayscale(.35) contrast(1.03) brightness(.86);
    transform:scale(1.0); will-change:transform; background:var(--bg-elev);
  }
  /* Ken Burns zoom is driven by .kb-active (not .active) so JS can freeze it
     mid-zoom via .kb-pause (animation-play-state:paused) on the outgoing
     slide instead of yanking the class that runs the animation — removing
     that class outright snaps transform back to scale(1) instantly, which
     shows as a hard jump while the slide is still fading out. Pausing just
     holds whatever zoom it had reached, which reads as seamless. */
  .hero-slide.kb.kb-active img, .hero-slide.kb.kb-active video{ animation:heroKenBurns linear forwards; animation-duration:var(--slide-duration, 7000ms); }
  .hero-slide.kb-pause img, .hero-slide.kb-pause video{ animation-play-state:paused; }
  @media (prefers-reduced-motion:no-preference){
    @keyframes heroKenBurns{ from{transform:scale(1.0);} to{transform:scale(1.09);} }
  }
  @media (prefers-reduced-motion:reduce){
    .hero-slide{transition:opacity .01s linear;}
    .hero-slide.kb.kb-active img, .hero-slide.kb.kb-active video{animation:none;}
  }
  .hero-slides::after{content:''; position:absolute; inset:0; z-index:1; background:linear-gradient(0deg, var(--bg) 0%, var(--hero-scrim-mid) 38%, var(--hero-scrim-top) 100%);}

  /* ---- dot pattern + grain overlays (subtle cinematic texture) ---- */
  .hero-dot-pattern{
    position:absolute; inset:0; z-index:1; pointer-events:none;
    background-image:radial-gradient(circle, rgba(255,255,255,.16) 1px, transparent 1.2px);
    background-size:7px 7px; opacity:.22; mix-blend-mode:soft-light;
    mask-image:linear-gradient(115deg, transparent 0%, #000 32%, #000 68%, transparent 100%);
    -webkit-mask-image:linear-gradient(115deg, transparent 0%, #000 32%, #000 68%, transparent 100%);
  }
  .hero-grain{
    position:absolute; inset:0; z-index:1; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  }
  @media(max-width:640px){ .hero-grain{display:none;} }

  /* ---- camera viewfinder overlay (REC) ---- */
  .hero-cam-overlay{position:absolute; inset:0; z-index:1; pointer-events:none;}
  .cam-rec{
    position:absolute; top:104px; inset-inline-start:6vw;
    display:flex; align-items:center; gap:9px;
    font-family:var(--font-mono); font-size:var(--text-2xs); letter-spacing:.22em; color:var(--ink-dim);
  }
  .cam-rec-dot{
    width:9px; height:9px; border-radius:50%; background:var(--rec-red);
    box-shadow:0 0 8px rgba(229,72,77,.5);
    animation:camBlink 2.4s ease-in-out infinite;
  }
  .cam-meta{
    position:absolute; top:104px; inset-inline-end:6vw;
    font-family:var(--font-mono); font-size:var(--text-2xs); letter-spacing:.2em; color:var(--ink-faint);
  }
  @keyframes camBlink{ 0%,100%{opacity:.25;} 50%{opacity:1;} }
  @media (prefers-reduced-motion:reduce){ .cam-rec-dot{animation:none; opacity:.8;} }
  @media(max-width:760px){
    .cam-rec{top:86px; font-size:var(--text-2xs);}
    .cam-meta{display:none;}
  }

  /* ---- prev/next + progress ---- */
  .hero-arrow{
    position:absolute; top:50%; transform:translateY(-50%); z-index:3;
    width:46px; height:46px; min-width:44px; min-height:44px; border-radius:50%;
    border:1px solid var(--line); background:var(--ctrl-bg); color:var(--ink-dim);
    font-size:1.4rem; font-weight:300; display:flex; align-items:center; justify-content:center;
    transition:border-color .25s, color .25s, background .25s;
  }
  .hero-arrow:hover{border-color:var(--accent); color:var(--accent); background:var(--ctrl-bg-hover);}
  .hero-prev{inset-inline-start:4vw;}
  .hero-next{inset-inline-end:4vw;}
  /* Slide indicator — a timecode ruler rather than a progress bar: short thin
     strokes on a shared baseline, the active one simply grows taller and turns
     gold. Deliberately no fill animation; the only motion is the handover
     between two strokes, which keeps the hero quiet while a slide plays.
     Each stroke is a 1px <span class="fill"> centred inside a transparent
     34px hit box, so the touch target stays finger-sized without the mark
     itself getting any heavier. */
  .hero-progress{position:absolute; bottom:46px; inset-inline-end:6vw; z-index:3; display:flex; align-items:flex-end; gap:9px;}
  .hero-progress-seg{
    width:14px; height:34px; background:none; cursor:pointer;
    display:flex; align-items:flex-end; justify-content:center;
  }
  .hero-progress-seg .fill{
    display:block; width:1px; height:6px; background:var(--ink-faint); opacity:.25;
    transition:height .52s cubic-bezier(.16,.84,.28,1), opacity .52s ease, background-color .52s ease;
  }
  .hero-progress-seg.done .fill{opacity:.45;}
  .hero-progress-seg.active .fill{height:14px; background:var(--accent); opacity:1;}
  .hero-progress-seg:hover .fill{opacity:.7;}
  .hero-progress-seg.active:hover .fill{opacity:1;}
  @media (prefers-reduced-motion:reduce){ .hero-progress-seg .fill{transition:none;} }
  @media(max-width:760px){ .hero-arrow{display:none !important;} .hero-progress{inset-inline-end:6vw; inset-inline-start:6vw; justify-content:center;} }

  /* Vertical nudge for the whole hero copy block, set from the admin panel
     (negative = higher up). A transform rather than a margin on purpose: the
     hero is a centred flex column, so shifting the box would also move the
     centring origin and fight itself — translateY moves only the paint. */
  /* --hero-offset-y-eff is written per-element by clampHeroOffset() once layout
     is measurable; the raw admin value stays as the fallback so the block is
     already in the right place on the very first paint, before any JS runs. */
  .hero-content{position:relative; z-index:2; max-width:640px; transform:translateY(var(--hero-offset-y-eff, var(--hero-offset-y, 0px)));}
  .hero-ctas{display:flex; gap:16px; flex-wrap:wrap; margin-top:30px; opacity:0; animation:rise 1s .65s forwards;}

  /* ---- the hero copy block is a fixed skeleton ----
     Headline and the Latin strapline under it are the same on every slide and
     must not move by a pixel when the slide changes; only the per-slide caption
     at the bottom swaps. Everything above that caption therefore has a RESERVED
     height rather than an intrinsic one:

       .hero-title-slot two lines, headline absolutely positioned inside it
       .hero-ensub      one line, never wraps
       .hero-lede-slot  one line, caption absolutely positioned inside it

     That is also what fixes the "headline goes three lines and rides up under
     the header" report: with the height fixed the centred flex column can't
     re-centre itself around a taller block, so the copy stays put whatever the
     browser does with the line breaking. */

  /* Two lines' worth of height, computed from the font size and line-height
     rather than measured — so it is identical before and after the webfont
     lands, and identical across engines. text-wrap:balance is deliberately NOT
     used any more: browsers without it were breaking the same headline into
     three lines while browsers with it made two, which is exactly the reported
     bug. The headline is absolutely positioned inside (same idiom as
     .hero-lede-slot) rather than clipped with overflow:hidden — clipping would
     cut Persian descenders, which overflow the line box at these line-heights. */
  /* min-height, not height. The reserved two lines are what keep the block
     from shifting as slides change — but they were a hard ceiling too, and the
     headline is absolutely positioned, so a half that needs a second line
     simply spilled out of the slot and landed on top of the strapline below.
     Persian fits two lines exactly, which is why it was never caught; the
     English copy takes three ("I don't capture moments;" wraps at this size),
     and so would any longer Persian headline typed into the admin.
     As a floor the guarantee still holds — the slot never gets shorter than
     two lines, and the headline is identical on every slide, so nothing moves
     when the carousel advances. It only grows on a language switch. */
  .hero-title-slot{
    position:relative;
    min-height:calc(var(--hero-title-size, clamp(2.1rem, 1.5rem + 2.6vw, 4.2rem)) * var(--hero-title-lh, 1.25) * 2);
  }
  .hero h1{
    /* Deliberately smaller than --heading-lg (the section-heading scale): the
       hero reads as a quiet opening line rather than a billboard, so it gets
       its own ramp — ~34px on a phone, ~60px on a wide desktop, against the
       45/85px it used to run at. --hero-title-size (admin) overrides the ramp. */
    /* In flow, so the slot above can grow to fit it. Kept out of `absolute`
       for that reason alone; nothing else depended on it — clampHeroOffset
       measures .hero-content, which now reports the true height. */
    position:relative; width:100%;
    font-weight:600;
    font-size:var(--hero-title-size, clamp(2.1rem, 1.5rem + 2.6vw, 4.2rem));
    line-height:var(--hero-title-lh, 1.25); letter-spacing:-.01em;
    margin:0; opacity:0; animation:rise 1s .3s forwards;
  }
  [dir="ltr"] .hero h1{ font-family:var(--font-en), var(--font-fa); letter-spacing:-.02em; }
  /* Each half on its own line at every width, not just on a phone. The default
     headline is written as two clauses, so this is the break the copy asks for
     — and pinning it here is what makes the reserved height above exact. */
  .hero h1 .ht-a, .hero h1 .accent-word{display:block;}
  .hero h1 .accent-word{color:var(--accent); font-weight:600;}

  /* ---- Latin strapline (one line, identical on every slide) ----
     Keeps its Latin face and tracking in both languages, like .section-sub:
     it is an English line by intent, not a translated one. The leading rule is
     a ::before so the markup stays a single text node, and the flex row inherits
     the page direction — in RTL the rule sits to the right of the text, where
     the line starts. */
  .hero-ensub{
    display:flex; align-items:center; gap:12px;
    margin-top:var(--hero-ensub-gap, 16px);
    font-family:var(--font-en), sans-serif;
    font-size:var(--hero-ensub-size, var(--text-2xs));
    font-weight:400; letter-spacing:.2em; text-transform:uppercase;
    /* --ink-dim, not --ink-faint: this line sits over the slide photo, and a
       bright frame (a daylight sky) wiped the faint tone out completely on a
       phone. Hierarchy against the caption below is carried by the caps and the
       tracking instead of by tone. */
    color:var(--ink-dim); line-height:1.5;
    opacity:0; animation:rise 1s .45s forwards;
  }
  .hero-ensub span{
    /* isolate, not direction:ltr on the row — the row must keep the page's
       direction so the block still starts on the correct side in RTL */
    direction:ltr; unicode-bidi:isolate;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;
  }
  .hero-ensub::before{content:''; flex:none; width:26px; height:1px; background:var(--accent); opacity:.85;}
  /* unset in the admin: the whole row (and its gap) disappears cleanly */
  .hero-ensub:empty{display:none;}

  /* Slide caption (location / project name, set per slide in the admin). Not a
     tagline — it names what is actually on screen, so it's typeset small and quiet.

     The slot holds a constant one-line height and the caption is absolutely
     positioned inside it. The hero is a centred flex column, so its contents are
     laid out around the block's total height — a caption that collapsed to zero
     on captionless slides changed that height and visibly shifted the headline
     on every slide change. With the height reserved, the headline is pinned and
     only the caption itself animates in and out.
     All three live inside .hero-content, so the dashboard's vertical offset moves
     the headline, the strapline and the caption together as one unit. */
  .hero-lede-slot{
    position:relative;
    margin-top:var(--hero-lede-gap, 14px);
    height:calc(var(--hero-lede-size, var(--text-base)) * var(--line-height-relaxed));
  }
  .hero p.lede{
    position:absolute; top:0; inset-inline-start:0; margin:0;
    max-width:60ch; color:var(--ink-dim);
    font-size:var(--hero-lede-size, var(--text-base)); font-weight:300;
    line-height:var(--line-height-relaxed);
    /* rise + de-blur, so the caption resolves into focus as the slide settles
       rather than just appearing. Only opacity/transform/filter animate — none
       of them affect layout, so the pinned block above cannot be disturbed. */
    opacity:0; transform:translateY(9px); filter:blur(5px);
    transition:opacity .55s ease, transform .6s cubic-bezier(.16,.84,.28,1), filter .55s ease;
  }
  .hero p.lede.on{opacity:1; transform:translateY(0); filter:blur(0);}
  @media (prefers-reduced-motion:reduce){
    .hero p.lede{transition:none; transform:none; filter:none;}
  }
  @keyframes rise{ from{opacity:0; transform:translateY(14px);} to{opacity:1; transform:translateY(0);} }
  @media (prefers-reduced-motion:reduce){ .hero h1, .hero p.lede, .hero .hero-ensub, .hero .eyebrow, .hero-ctas{animation:none; opacity:1;} }

  /* ---- hero on a phone ----
     Both CTAs are dropped — the sticky nav already carries the same "start a
     project" call (and now a one-tap shortcut to it), so repeating it here only
     crowded the frame. The two-line headline pin is no longer part of this
     block: it applies at every width now. */
  @media(max-width:700px){
    .hero-title-slot{
      min-height:calc(var(--hero-title-size-mobile, clamp(1.5rem, 6vw, 2rem)) * var(--hero-title-lh-mobile, 1.35) * 2);
    }
    .hero h1{
      font-size:var(--hero-title-size-mobile, clamp(1.5rem, 6vw, 2rem));
      line-height:var(--hero-title-lh-mobile, 1.35);
    }
    .hero-ensub{
      margin-top:var(--hero-ensub-gap-mobile, 13px);
      font-size:var(--hero-ensub-size-mobile, var(--text-2xs));
      letter-spacing:.15em; gap:9px;
    }
    .hero-ensub::before{width:18px;}
    .hero-lede-slot{
      margin-top:var(--hero-lede-gap-mobile, 11px);
      height:calc(var(--hero-lede-size-mobile, var(--text-base)) * var(--line-height-relaxed));
    }
    .hero p.lede{font-size:var(--hero-lede-size-mobile, var(--text-base));}
    .hero-ctas{display:none;}
    .hero-content{transform:translateY(var(--hero-offset-y-eff, var(--hero-offset-y-mobile, 0px)));}
  }
  /* The strapline is a one-liner by contract, so it ellipsises rather than
     wraps when a long string meets a narrow screen. Trimming the tracking and
     the size here is what keeps the default copy inside a 320px phone instead
     of landing on that fallback. */
  @media(max-width:400px){
    .hero-ensub{font-size:var(--hero-ensub-size-mobile, 11px); letter-spacing:.08em; gap:7px;}
    .hero-ensub::before{width:14px;}
  }

  /* ---------- SECTION GENERIC ---------- */
  section{
    padding-block:var(--section-space-desktop);
    padding-inline:max(var(--container-pad), calc((100% - var(--container-width)) / 2));
    position:relative;
  }
  .section-head{margin-bottom:80px;}
  .section-head .eyebrow{margin-bottom:18px;}
  .section-head h2{font-weight:600; font-size:var(--heading-md); line-height:var(--line-height-tight); letter-spacing:-.005em;}
  [dir="ltr"] .section-head h2{ font-family:var(--font-en), var(--font-fa); }
  .section-head .row{display:flex; align-items:flex-end; justify-content:space-between; flex-wrap:wrap; gap:24px;}
  /* optional thin line under a section heading (admin-editable). Unlike
     .eyebrow / .row-label there is deliberately NO [dir="rtl"] reset of the
     Latin face and tracking here: this line is meant to carry a short English
     strapline underneath the Persian heading, so it keeps the Latin styling in
     both languages. :empty keeps the spacing untouched when the field is unset. */
  .section-head .section-sub{
    display:block; margin-top:14px; font-family:var(--font-en), sans-serif;
    font-size:var(--text-2xs); font-weight:400; letter-spacing:.16em;
    text-transform:uppercase; color:var(--ink-faint); line-height:1.6;
  }
  .section-head .section-sub:empty{display:none;}
  .reveal{opacity:0; transform:translateY(28px); transition:opacity 1s ease, transform 1s ease;}
  .reveal.in{opacity:1; transform:translateY(0);}

  /* ---------- ABOUT ---------- */
  /* editorial two-column: the portrait column is sticky so it stays in frame
     while a long bio scrolls beside it (align-items:start is what lets the
     sticky child actually travel — with `center` it can't move) */
  .about-grid{display:grid; grid-template-columns:.8fr 1.2fr; gap:80px; align-items:start;}
  /* matches the 1080px breakpoint below, where the grid collapses to one
     column and a sticky portrait would just pin awkwardly above the text */
  @media(min-width:1081px){
    .about-portrait-col{position:sticky; top:96px;}
  }
  .about-portrait{
    aspect-ratio:4/5; overflow:hidden; background:var(--bg-elev); border-radius:var(--radius-md);
    border:1px solid var(--line);
  }
  .about-portrait img{width:100%; height:100%; object-fit:cover; filter:grayscale(.25) contrast(1.02);}
  /* scrim so the caption stays legible over any portrait */
  .about-portrait::after{
    content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
    background:linear-gradient(to top, rgba(0,0,0,.62), rgba(0,0,0,.12) 38%, transparent 60%);
  }
  /* Physical bottom-LEFT on purpose (not inset-inline-start): the name is set
     in Latin script and reads left-to-right, so it is pinned to the same
     corner in both the RTL and LTR versions of the site. */
  .about-cap{
    position:absolute; z-index:2; bottom:0; left:0; padding:22px 24px;
    direction:ltr; text-align:left;
  }
  .about-cap-name{
    font-family:var(--font-en), sans-serif; font-size:var(--text-xl); font-weight:600;
    color:var(--ink); letter-spacing:-.01em; line-height:1.2;
  }
  .about-cap-role{
    font-family:'IBM Plex Mono', monospace; font-size:var(--text-2xs); color:var(--accent);
    letter-spacing:.16em; text-transform:uppercase; margin-top:7px;
  }
  /* optional stat strip under the portrait — hidden entirely when unset.
     No boxed grid: just a hairline that fades out at both ends, hairline
     dividers between figures, and the numbers carrying a soft warm bloom, so
     it reads like a title card rather than a table. */
  .about-stats{
    position:relative; display:grid; grid-template-columns:repeat(auto-fit, minmax(74px,1fr));
    margin-top:26px; padding-top:24px;
  }
  .about-stats:empty{display:none;}
  .about-stats::before{
    content:''; position:absolute; top:0; inset-inline:0; height:1px;
    background:linear-gradient(90deg, transparent, rgba(var(--accent-rgb,201,164,104),.42), transparent);
  }
  .about-stat{position:relative; text-align:center; padding:2px 8px;}
  /* divider between figures, faded at both ends so it never reads as a table rule */
  .about-stat + .about-stat::before{
    content:''; position:absolute; inset-block:12%; inset-inline-start:0; width:1px;
    background:linear-gradient(180deg, transparent, var(--line), transparent);
  }
  .about-stat b{
    display:block; font-family:'IBM Plex Mono', monospace; font-weight:500;
    font-size:clamp(1.3rem, 2.1vw, 1.7rem); line-height:1.1; color:var(--accent);
    letter-spacing:-.02em; font-variant-numeric:tabular-nums;
    text-shadow:0 0 24px rgba(var(--accent-rgb,201,164,104),.3);
  }
  .about-stat span{
    display:block; margin-top:9px; font-size:var(--text-2xs); color:var(--ink-faint);
    letter-spacing:.16em; text-transform:uppercase; line-height:1.5;
  }
  /* each figure lifts in on reveal, one after the next */
  .about-stat{opacity:0; transform:translateY(12px);}
  .about-portrait-col.in .about-stat{
    animation:aboutStatIn .7s cubic-bezier(.22,.61,.36,1) forwards;
    animation-delay:calc(var(--si, 0) * 110ms + 220ms);
  }
  @keyframes aboutStatIn{ to{ opacity:1; transform:translateY(0); } }
  @media(prefers-reduced-motion:reduce){
    .about-stat{opacity:1; transform:none;}
    .about-portrait-col.in .about-stat{animation:none;}
  }

  .about-copy .eyebrow{margin-bottom:16px; display:block;}
  /* one step down from --heading-md: at full width that token renders ~59px,
     which made this heading louder than the site's own section headlines */
  /* Every type size in this section is a var with the previous hard-coded value
     as its fallback, so the admin panel can override any of them (Info tab →
     «اندازه متن‌های بخش درباره من») without the section falling apart when the
     fields are left empty. */
  .about-copy h2{font-size:var(--about-heading-size, var(--heading-sm)); font-weight:600; line-height:var(--line-height-tight); margin-bottom:24px;}
  [dir="ltr"] .about-copy h2{ font-family:var(--font-en), var(--font-fa); }
  .about-bio p{color:var(--ink-dim); margin-bottom:17px; font-size:var(--about-bio-size, var(--text-base)); line-height:var(--line-height-relaxed); max-width:64ch; font-weight:400;}
  .about-bio p:last-child{margin-bottom:0;}
  /* the opening paragraph leads by tone, not bulk: same size as the body,
     just brighter and a touch heavier */
  .about-bio p:first-child{color:var(--ink); font-weight:500; line-height:1.95;}
  .about-bio p b, .about-bio p strong{color:var(--accent); font-weight:600;}

  /* Secondary copy under the bio: its own admin field, one step smaller and
     dimmer than the body so it reads as a footnote / extra detail rather than
     as another bio paragraph. It sits INSIDE .about-bio-clip on purpose, so the
     "ادامه‌ی متن" expander measures and reveals it together with the bio instead
     of leaving it stranded below the collapsed block. */
  .about-note{margin-top:26px; padding-top:20px; border-top:1px solid var(--line);}
  .about-note:empty{display:none;}
  .about-note p{
    color:var(--ink-faint); font-size:var(--about-note-size, var(--text-sm));
    line-height:var(--line-height-normal); max-width:64ch; font-weight:400;
    margin-bottom:12px;
  }
  .about-note p:last-child{margin-bottom:0;}
  .about-note p b, .about-note p strong{color:var(--ink-dim); font-weight:600;}

  /* ---- "ادامه‌ی متن" expander ----
     max-height is animated to a measured pixel value (set from JS) rather than
     a guessed constant, so the reveal eases all the way open no matter how
     long the bio is. The button only appears when the text actually overflows. */
  .about-bio-wrap{position:relative;}
  .about-bio-clip{overflow:hidden; transition:max-height .85s cubic-bezier(.22,.61,.36,1);}
  .about-bio-wrap:not(.open) .about-bio-clip{max-height:var(--about-clamp, 300px);}
  .about-bio-wrap.open .about-bio-clip{max-height:var(--about-full, 4000px);}
  .about-bio-wrap:not(.clamped) .about-bio-clip{max-height:none;}
  .about-bio-fade{
    position:absolute; inset-inline:0; bottom:46px; height:120px; pointer-events:none;
    background:linear-gradient(to top, var(--bg), rgba(10,10,10,.7) 48%, transparent);
    opacity:0; transition:opacity .5s ease;
  }
  .about-bio-wrap.clamped:not(.open) .about-bio-fade{opacity:1;}
  .about-more{
    margin-top:18px; display:inline-flex; align-items:center; gap:11px;
    background:none; border:0; cursor:pointer; padding:10px 0;
    font-family:var(--font-fa); font-size:var(--text-sm); color:var(--accent);
    transition:color var(--transition-normal);
  }
  /* an author `display` beats the UA stylesheet's [hidden]{display:none}, so
     without this the button stays on screen for short bios that never clamp */
  .about-more[hidden]{display:none;}
  .about-more:hover{color:var(--color-accent-hover, #dab97e);}
  .about-more .ln{width:34px; height:1px; background:currentColor; opacity:.5;}
  .about-more .chev{font-family:'IBM Plex Mono', monospace; transition:transform .45s cubic-bezier(.22,.61,.36,1);}
  .about-bio-wrap.open .about-more .chev{transform:rotate(180deg);}
  @media(prefers-reduced-motion:reduce){
    .about-bio-clip{transition:none;}
    .about-bio-fade{transition:none;}
    .about-more .chev{transition:none;}
  }

  .resume-block{margin-top:44px;}
  .resume-label{display:block; font-size:var(--about-resume-label-size, var(--text-2xs)); color:var(--ink-faint); letter-spacing:.14em; text-transform:uppercase; margin-bottom:22px;}
  /* Persian can't take the mono face or the tracking — same reasoning as
     [dir="rtl"] .eyebrow further up. */
  [dir="rtl"] .resume-label, [dir="rtl"] .resume-label.mono{
    font-family:var(--font-fa), sans-serif; letter-spacing:0; text-transform:none; font-weight:300;
  }
  .resume-list{position:relative; padding-inline-start:26px; border-inline-start:1px solid var(--line);}
  .resume-item{position:relative; padding:0 0 30px;}
  .resume-item:last-child{padding-bottom:0;}
  .resume-item::before{
    content:''; position:absolute; top:6px; inset-inline-start:-31px; width:9px; height:9px; border-radius:50%;
    background:var(--bg); border:2px solid var(--accent);
  }
  /* The year is always rendered in Latin digits (toLatinDigits() at render), so
     the mono face actually has glyphs for it — with Persian digits it fell back
     mid-run to a different font, which is what made this line look wrong. */
  .resume-item .yr{
    font-family:var(--font-mono); color:var(--accent);
    font-size:var(--about-year-size, var(--text-2xs)); letter-spacing:.08em;
    display:block; margin-bottom:6px; direction:ltr; unicode-bidi:isolate;
  }
  [dir="rtl"] .resume-item .yr{text-align:right;}
  .resume-item .role{font-weight:500; margin-bottom:5px; font-size:var(--about-role-size, var(--text-base));}
  .resume-item .desc{color:var(--ink-dim); font-size:var(--about-desc-size, var(--text-sm)); font-weight:400; line-height:var(--line-height-normal);}

  /* ---------- FILTER CHIPS ---------- */
  .chips{display:flex; gap:10px; flex-wrap:wrap; align-items:center;}
  /* These were mono + .1em tracking + uppercase because the only label that
     ever appeared was the Latin More/Less one. Now that videos carry Persian
     categories, those same rules applied to Persian: a face with no Persian
     glyphs, tracking that breaks the joins, and an uppercase transform that
     means nothing in the script. Category chips get the real Persian face;
     mono is kept for the one chip that is genuinely Latin (.is-latin). */
  .chip{
    color:var(--ink-faint); font-family:var(--font-fa), var(--font-en);
    font-size:var(--text-2xs); letter-spacing:0; line-height:1.7;
    padding:7px 15px; border:1px solid var(--line); border-radius:999px;
    white-space:nowrap; transition:color .2s, border-color .2s, background .2s;
  }
  [dir="ltr"] .chip{font-family:var(--font-en), var(--font-fa);}
  .chip.is-latin{font-family:var(--font-mono); letter-spacing:.1em; text-transform:uppercase;}
  .chip:hover{color:var(--ink); border-color:var(--line-strong, var(--ink-faint));}
  .chip.active{color:var(--accent); border-color:var(--accent); background:rgba(201,164,104,.08);}
  /* The sort toggle sits in the same row as the category filters but does a
     different job — it stays lit while you move between categories. The arrow
     (newest first) and the hairline before it are what say "not a category",
     without pulling it out into a second row of controls. */
  .chip-sort{display:inline-flex; align-items:center; gap:6px; margin-inline-start:6px; position:relative;}
  .chip-sort::before{
    content:""; position:absolute; inset-inline-start:-9px; top:20%; bottom:20%;
    width:1px; background:var(--line);
  }
  .chip-sort::after{content:"\2193"; font-family:var(--font-mono); font-size:.9em; opacity:.75;}
  @media(max-width:700px){
    /* Three chips at 16-25 Persian characters each wrap raggedly on a phone.
       One scrollable row keeps them on a single line and reachable. */
    .chips{
      flex-wrap:nowrap; overflow-x:auto; gap:8px;
      scrollbar-width:none; -ms-overflow-style:none; padding-bottom:2px;
    }
    .chips::-webkit-scrollbar{display:none;}
    .chip{flex:0 0 auto; padding:6px 13px;}
  }

  /* ---------- VIDEO GALLERY ---------- */
  .video-row-block{margin-top:56px;}
  .video-row-block:first-of-type{margin-top:0;}
  .row-label{display:block; font-size:var(--text-2xs); color:var(--ink-faint); letter-spacing:.14em; text-transform:uppercase; margin-bottom:18px;}
  /* Same reasoning as [dir="rtl"] .eyebrow above — and this one carries .mono
     too, whose own .12em tracking has to be unset as well. Colour stays
     var(--ink-faint) exactly as before. */
  [dir="rtl"] .row-label, [dir="rtl"] .row-label.mono{
    font-family:var(--font-fa), sans-serif; letter-spacing:0; text-transform:none; font-weight:300;
  }
  .grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(300px,1fr)); gap:22px; margin-top:0; align-items:start;}
  .video-row--portrait{grid-template-columns:repeat(auto-fill, minmax(200px,1fr));}
  .card{
    position:relative; aspect-ratio:16/9; overflow:hidden; cursor:pointer; background:var(--bg-elev);
    border-radius:16px; transition:transform .4s cubic-bezier(.2,.8,.2,1), box-shadow .4s ease;
    /* lifts the cover tile above the global grain layer (body::after, 400) so
       thumbnails stay clean, while staying under the sticky nav (500). */
    z-index:401;
  }
  .card.card--portrait{aspect-ratio:9/16;}
  .card.card--landscape{aspect-ratio:16/9;}
  .card:hover{transform:translateY(-6px) scale(1.015); box-shadow:0 22px 46px -18px rgba(0,0,0,.75);}
  /* <picture> is inline by default, so the img's height:100% below would have
     nothing to resolve against once a WebP source is present. Same fix as
     .hero-slide picture. */
  .card picture{display:block; width:100%; height:100%;}
  .card img{
    width:100%; height:100%; object-fit:cover; filter:grayscale(.45) contrast(1.02);
    opacity:0; transition:opacity .5s ease, transform 1.1s cubic-bezier(.2,.7,.2,1), filter 1s;
  }
  /* Cards are lazy-loaded, so the thumbnail often finishes fetching well after
     the card itself has already slid/scaled into place (see cardRiseIn) — the
     raw <img> used to just snap into view the instant it decoded, which read
     as a second, uncontrolled "jump" layered on top of the entrance motion.
     This fades the image in on its own once loaded (see wireCardImageFade). */
  .card img.img-loaded{opacity:1;}
  /* The card already clips with overflow:hidden, but that clip is unreliable
     for the portrait row: it lives in a horizontal scroll container, which
     promotes the card to its own composited layer, and the filtered <img>
     inside then paints its top corners square. Giving the children the same
     radius makes the rounding independent of whether the ancestor clip holds. */
  .card img, .card .scrim{border-radius:inherit;}
  .card:hover img{transform:scale(1.08); filter:grayscale(0) contrast(1.05);}
  /* entrance: a soft rise-and-settle so the gallery feels alive on first
     scroll-into-view. Uses a keyframe (not the transition above) so it doesn't
     slow down hover once the card has already settled in.

     Deliberately translate-ONLY, no scale: every thumbnail carries a
     `filter: grayscale()`, and scaling a filtered raster forces the browser to
     re-run the filter and re-rasterise the image on every single frame, which
     showed up as a small hitch mid-entrance. A pure translate stays on the
     compositor, so the same motion now costs almost nothing. `will-change` is
     set only for the duration of the entrance and dropped again on settle
     (see the animationend handler in renderVideoGrid) so we don't leave dozens
     of permanently-promoted layers sitting in memory. */
  .card--extra{display:none;}
  .card.stagger-item{opacity:0; transform:translateY(30px);}
  /* will-change is scoped to the animating window only: on `.in` (the moment
     the entrance starts) and off again on settle. Putting it on .stagger-item
     instead would leave a promoted layer alive for every card still waiting
     below the fold. The settled rule needs the extra class to outrank this. */
  .card.stagger-item.in{
    animation:cardRiseIn .9s cubic-bezier(.22,.61,.36,1) forwards;
    animation-delay:var(--stagger-delay, 0ms);
    will-change:transform, opacity;
  }
  .card.stagger-item.in.card--settled{will-change:auto;}
  @keyframes cardRiseIn{ to{ opacity:1; transform:translateY(0); } }
  .card .scrim{
    position:absolute; inset:0; display:flex; flex-direction:column; justify-content:flex-end; padding:22px;
    /* Reaches a little higher than the text block ever does, so a title line
       is never left sitting on an unshaded, bright part of the thumbnail. */
    background:linear-gradient(to top, rgba(0,0,0,.86), rgba(0,0,0,.45) 34%, rgba(0,0,0,0) 62%);
    opacity:0; transition:opacity .4s;
  }
  .card:hover .scrim{opacity:1;}
  /* One line, always. A portrait card is at most 240px wide (see
     --portrait-card-w) and this used to be set in --heading-card, i.e. up to
     29.6px — a descriptive title then wrapped onto three or four lines, grew
     upward past the top of the scrim gradient, and collided with the meta row
     underneath it. Titles are meant to be specific and therefore long, so the
     card truncates and the full text lives where there is room for it: the
     lightbox, the /projects/<slug> <h1>, and the title attribute on hover. */
  .card-title{
    font-weight:400; font-size:clamp(1rem, .95rem + .25vw, 1.15rem);
    line-height:1.55; margin-bottom:5px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  /* Same reason as .lb-meta: a Persian category set in a Latin monospace face
     with letter-spacing renders as mismatched, disconnected glyphs. */
  .card-meta{
    display:flex; flex-wrap:nowrap; align-items:center; gap:4px 14px;
    font-family:var(--font-fa), var(--font-en); font-size:var(--text-2xs);
    color:var(--ink-dim); letter-spacing:0;
    overflow:hidden;
  }
  /* Location is the first item and the one most likely to be long; let it be
     the one that shrinks, so the year never gets pushed out of the card. */
  .card-meta .lb-meta-item{min-width:0; flex-shrink:1;}
  .card-meta .lb-meta-item span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  /* The date is short and is what the row exists to say — it never gets
     squeezed. Keyed off .is-year rather than .num because a Persian month name
     ("خرداد ۱۴۰۴") deliberately does NOT carry .num: mono has no Persian
     glyphs. */
  .card-meta .lb-meta-item.is-year{flex-shrink:0;}
  /* A portrait card is ~225px wide, which is not enough for three meta items
     without all of them ending in an ellipsis. Category is the one to drop:
     the portrait row is already labelled as reels/stories, and in practice
     nearly every card in it repeats the same one or two categories, so it
     distinguishes nothing. Location and year actually differ per video, and
     the full set is still shown in the lightbox and on the project page. */
  .card--portrait .card-meta .is-cat{display:none;}
  [dir="ltr"] .card-meta{font-family:var(--font-en), var(--font-fa);}
  .card-meta .lb-meta-item svg{width:13px; height:13px;}
  .card-meta:empty{display:none;}
  .play-ring{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  }
  .play-ring span{
    width:48px; height:48px; border-radius:50%; border:1px solid rgba(255,255,255,.6);
    display:flex; align-items:center; justify-content:center; opacity:0; transform:scale(.85); transition:opacity .35s, transform .35s;
    background:rgba(0,0,0,.25); backdrop-filter:blur(2px);
  }
  .card:hover .play-ring span{opacity:1; transform:scale(1);}

  .empty-state{padding:60px 20px; text-align:center; border:1px dashed var(--line); color:var(--ink-faint); font-family:'IBM Plex Mono'; font-size:var(--text-2xs); border-radius:16px;}
  .videos-showall-row{display:flex; justify-content:center; margin-top:48px;}

  /* ---------- MOBILE GALLERY (tidy layout) ---------- */
  @media(max-width:700px){
    .video-row-block{margin-top:40px;}
    .grid.video-row{grid-template-columns:1fr; gap:16px;}
    .card{border-radius:14px;}
    .card .scrim{
      opacity:1; padding:16px;
      background:linear-gradient(to top, rgba(0,0,0,.78), rgba(0,0,0,0) 58%);
    }
    .card-title{font-size:1.02rem;}
    .play-ring span{opacity:1; transform:scale(.88); width:42px; height:42px;}
    /* Portrait videos: horizontal snap carousel instead of giant stacked covers.

       Cards snap to the START edge and the track keeps the normal container
       padding, so the first card lines up with its row label and with every
       other section on the page. (An earlier version snapped cards centred,
       which forced the track to be padded by half the leftover width — that
       left a ~79px blank gutter before the first card at the resting scroll
       position and read as a layout bug.) scroll-padding-inline mirrors that
       padding so every later card also comes to rest exactly on the container
       edge rather than tucked under it.
       scroll-snap-stop is deliberately NOT set: a flick should still be able to
       carry across several cards. */
    .grid.video-row--portrait{
      --portrait-card-w:min(58vw, 240px);
      display:flex; overflow-x:auto; scroll-snap-type:x mandatory; gap:12px;
      margin-inline:calc(-1 * var(--container-pad));
      padding-inline:var(--container-pad);
      scroll-padding-inline:var(--container-pad);
      padding-bottom:8px; scrollbar-width:none; -ms-overflow-style:none;
      overscroll-behavior-x:contain;
      /* overflow-x:auto silently promotes overflow-y from `visible` to `auto`,
         so anything that overflows vertically makes the track scrollable on the
         cross axis too — the entrance transform below (translateY(30px), still
         applied to every card that has not been scrolled into view yet) let you
         nudge the whole row ~30px up and down with a trackpad. Clip that axis,
         and drop the two transforms that would then be clipped instead: the
         entrance becomes fade-only in the carousel and the hover lift is
         meaningless on a touch-scrolled row anyway. */
      overflow-y:hidden;
    }
    .grid.video-row--portrait::-webkit-scrollbar{display:none;}
    .grid.video-row--portrait .card{flex:0 0 var(--portrait-card-w); scroll-snap-align:start;}
    .grid.video-row--portrait .card.stagger-item{transform:none;}
    .grid.video-row--portrait .card:hover{transform:none;}
  }

  /* ---------- PORTRAIT RING — the covers on a draggable cylinder ---------- */
  /* Second view for the portrait row. Geometry lives in JS (the radius has to
     be computed from the card width and the item count, which CSS can't do);
     everything below is the look, plus the two rules that switch views. */
  /* the label keeps its own 18px gap to the row — it just moves up to the line
     now that the view toggle shares it */
  .row-label-line{
    display:flex; align-items:baseline; justify-content:space-between;
    gap:18px; flex-wrap:wrap; margin-bottom:18px;
  }
  .row-label-line .row-label{margin-bottom:0;}
  .pring-toggle{
    background:none; border:1px solid var(--line); border-radius:999px; cursor:pointer;
    color:var(--ink-dim); font-family:var(--font-fa); font-size:var(--text-2xs);
    padding:5px 14px; transition:color var(--transition-fast), border-color var(--transition-fast);
  }
  [dir="ltr"] .pring-toggle{font-family:var(--font-en), var(--font-fa);}
  .pring-toggle:hover{color:var(--accent); border-color:var(--accent);}

  #portraitBlock.view-ring .grid.video-row--portrait{display:none;}
  #portraitBlock.view-grid .pring-wrap{display:none;}

  /* The cards deliberately overhang the viewport on both sides, and absolutely
     positioned boxes still count towards inline overflow — without this the
     whole page picks up a horizontal scrollbar on a phone. `clip` rather than
     `hidden`: hidden would make this a scroll container, which flattens the
     preserve-3d chain inside it. */
  .pring-wrap{overflow-x:clip;}
  /* Height has to clear the tallest card the JS will build plus the ~5% the
     front one is scaled up by: a 380px card is 676px of 9:16, so 780px is the
     floor for the desktop cap. The vh half is what keeps a short laptop safe —
     at 1440x800 it resolves to 672px and the card scales down to suit. */
  .pring{
    position:relative; width:100%; height:min(84vh, 780px);
    display:flex; align-items:center; justify-content:center;
    perspective:2000px; perspective-origin:50% 46%;
    touch-action:pan-y; user-select:none; cursor:grab;
  }
  .pring.is-dragging{cursor:grabbing;}
  .pring:focus-visible{outline:1px solid var(--accent); outline-offset:6px;}
  .pring-track{position:absolute; inset:0; transform-style:preserve-3d; will-change:transform;}
  .pring-card{
    position:absolute; left:50%; top:50%;
    width:var(--pcw); height:var(--pch);
    margin-left:calc(var(--pcw) / -2); margin-top:calc(var(--pch) / -2);
    transform-style:preserve-3d; cursor:pointer;
    /* transform only. Listing opacity and filter here as well pinned a full
       backing store per card for the life of the page, and a 1080x1920 cover
       is 7.9MB decoded — sixteen of those is 126MB of bitmap that iOS starts
       evicting, and an evicted layer paints black. */
    will-change:transform;
  }
  /* Depth without a filter. A per-card filter() forces the compositor to build
     an extra offscreen surface for every card on every frame, and inside a
     preserve-3d chain WebKit is liable to hand back a blank one. A plain
     opacity fade on a solid overlay costs nothing and cannot fail that way, so
     it carries the depth cue everywhere; the richer greyscale/blur treatment is
     layered on top only where there is a mouse (see .pring--rich). */
  .pring-veil{
    position:absolute; inset:0; border-radius:inherit; pointer-events:none;
    background:var(--color-bg); opacity:0;
  }
  .pring-frame{
    position:relative; width:100%; height:100%; overflow:hidden;
    border:1px solid var(--line); border-radius:16px;
    background:var(--bg-elev); box-shadow:0 30px 60px -28px rgba(0,0,0,.9);
    transition:border-color var(--transition-normal);
    /* A rounded overflow:hidden box being rotated in 3D has its corners clipped
       by the un-antialiased layer edge and they come back jagged. Its own
       composited layer gets the rounded mask rasterised with the layer. */
    isolation:isolate; -webkit-backface-visibility:hidden;
  }
  .pring-card.is-front .pring-frame{border-color:rgba(201,164,104,.55);}
  /* <picture> is inline by default, so the img's height:100% would have nothing
     to resolve against once a WebP source is present. Same fix as .card picture
     and .hero-slide picture. */
  .pring-frame picture{display:block; width:100%; height:100%;}
  /* radius repeated on the children rather than left to the parent's clip: mid
     rotation the corner mask and the child edge rasterise separately and a 1px
     hairline of unclipped image shows through the curve */
  .pring-frame img{
    position:absolute; inset:0; width:100%; height:100%; display:block;
    object-fit:cover; border-radius:inherit;
    filter:grayscale(.45) contrast(1.02); transition:filter .5s ease;
  }
  .pring-card.is-front .pring-frame img{filter:grayscale(0) contrast(1.05);}
  .pring-scrim{
    position:absolute; inset:0; border-radius:inherit;
    background:linear-gradient(to top, rgba(8,8,8,.92) 0%, rgba(8,8,8,.55) 26%, rgba(8,8,8,0) 58%);
  }
  /* Type scales off the card, not the viewport — the same card is ~250px wide
     on a desktop and ~170px on a phone, and a fixed size wrapped to three lines
     at the small end. */
  .pring-cap{
    position:absolute; inset-inline:0; bottom:0; text-align:start;
    padding:calc(var(--pcw) * .062) calc(var(--pcw) * .058) calc(var(--pcw) * .056);
  }
  .pring-title{
    font-family:var(--font-fa); font-weight:500; line-height:1.5; color:var(--ink);
    font-size:clamp(.78rem, calc(var(--pcw) * .064), 1.05rem);
    margin-bottom:4px;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  }
  [dir="ltr"] .pring-title{font-family:var(--font-en), var(--font-fa);}
  /* metadata goes through the shared .lb-meta-item icon row, same as every
     other card — never a dot-joined mono string (see the typography note) */
  .pring-cap .card-meta{font-size:clamp(.62rem, calc(var(--pcw) * .048), .78rem);}
  /* Same call as .card--portrait: a ~250px card can't hold three meta items
     without all three ending in an ellipsis, and category is the one that
     distinguishes nothing in a row that is already labelled. */
  .pring-cap .card-meta .is-cat{display:none;}
  .pring-play{
    position:absolute; top:12px; inset-inline-end:12px;
    width:32px; height:32px; border:1px solid rgba(244,242,238,.3); border-radius:50%;
    display:grid; place-items:center; background:var(--ctrl-bg);
    opacity:0; transition:opacity var(--transition-normal);
  }
  .pring-card.is-front .pring-play{opacity:1;}
  .pring-play svg{width:12px; height:12px; fill:var(--ink);}
  [dir="rtl"] .pring-play svg{transform:scaleX(-1);}
  .pring-hint{
    text-align:center; color:var(--ink-faint); font-size:var(--text-2xs);
    margin-top:10px; font-family:var(--font-fa);
  }
  [dir="ltr"] .pring-hint{font-family:var(--font-en), var(--font-fa);}
  @media (max-width:760px){
    .pring{height:min(58vh, 430px); perspective:1100px;}
  }
  /* Touch devices drop every filter in the ring. This one is on the <img>, so
     it forces its own offscreen surface per card on top of the card's — and it
     is the cheapest thing to lose, because the veil already carries the sense
     of depth. Matches the pringRich() gate in the JS. */
  @media (pointer: coarse){
    .pring-frame img,
    .pring-card.is-front .pring-frame img{filter:none; transition:none;}
  }
  /* No auto-spin for anyone who asked motion to stop — drag still works, so the
     view keeps all of its content, it just never moves on its own. */
  @media (prefers-reduced-motion:reduce){
    .pring-frame, .pring-frame img, .pring-play{transition:none;}
  }

  /* ---------- LIGHTBOX ---------- */
  .lightbox{position:fixed; inset:0; background:var(--overlay-bg); z-index:2000; display:none; align-items:center; justify-content:center; padding:6vw;}
  .lightbox.open{display:flex;}
  .lightbox-inner{max-width:1100px; width:100%; transition:opacity .28s ease, transform .28s ease;}
  .lightbox-inner.portrait{max-width:420px;}
  .lightbox-inner.switching{opacity:0; transform:scale(.97);}
  .lightbox-inner img{width:100%; max-height:76vh; object-fit:contain; border-radius:16px;}
  .lightbox-inner video, .lightbox-inner iframe{width:100%; aspect-ratio:16/9; background:#000; border:none; border-radius:16px; display:block;}
  .lightbox-inner.portrait video, .lightbox-inner.portrait iframe{aspect-ratio:9/16; max-height:80vh;}
  /* A radius on <video> alone is not honoured once playback starts: the decoded
     frames are composited by the platform player above the element's own box,
     so the corners square off mid-play (and iframes never clip their content
     at all). Clipping on the wrapper is what actually holds during playback. */
  #lbMedia{border-radius:16px; overflow:hidden; line-height:0;}
  /* The caption used to be one flex row in 'IBM Plex Mono' with letter-spacing
     applied to the whole thing. Both are wrong for Persian: the mono face
     carries no Persian glyphs, so every title fell back to a different face
     glyph-by-glyph, and letter-spacing prises apart a script whose letters are
     supposed to join. Title and metadata are now separate blocks — the title
     in the real Persian face at heading size, the metadata as small icon rows,
     and mono kept only for the year, which is the one genuinely Latin bit. */
  .lightbox-cap{margin-top:22px;}
  .lb-title{
    font-family:var(--font-fa), var(--font-en);
    font-size:var(--heading-card); font-weight:400; line-height:1.5; letter-spacing:0;
    color:var(--ink); margin:0;
  }
  [dir="ltr"] .lb-title{font-family:var(--font-en), var(--font-fa);}
  .lb-title:empty{display:none;}
  .lb-meta{
    display:flex; flex-wrap:wrap; align-items:center; gap:8px 20px; margin-top:12px;
    font-family:var(--font-fa), var(--font-en); font-size:var(--text-2xs);
    color:var(--ink-dim); letter-spacing:0;
  }
  [dir="ltr"] .lb-meta{font-family:var(--font-en), var(--font-fa);}
  .lb-meta:empty{display:none;}
  .lb-meta-item{display:inline-flex; align-items:center; gap:7px; line-height:1.7;}
  .lb-meta-item svg{width:15px; height:15px; flex-shrink:0; color:var(--accent);}
  /* Digits are Latin whatever the interface language, so the year is the one
     field the mono face actually renders correctly. */
  .lb-meta-item.num{font-family:'IBM Plex Mono', var(--font-fa); letter-spacing:.06em;}
  .lightbox-desc{margin-top:18px; color:var(--ink-dim); font-size:var(--text-base); font-weight:400; max-width:64ch; line-height:var(--line-height-relaxed);}
  .lightbox-desc:empty{display:none;}
  .lightbox-close{position:absolute; top:34px; inset-inline-end:6vw; color:var(--ink-dim); font-size:1.4rem; font-weight:200; z-index:2;}
  .lightbox-close:hover{color:var(--accent);}
  .lightbox-nav{
    position:absolute; top:50%; transform:translateY(-50%); z-index:2;
    width:52px; height:52px; border-radius:50%; border:1px solid var(--line); color:var(--ink-dim);
    background:var(--ctrl-bg); font-size:1.6rem; font-weight:300; display:flex; align-items:center; justify-content:center;
    transition:border-color .25s, color .25s, background .25s;
  }
  .lightbox-nav:hover{border-color:var(--accent); color:var(--accent); background:var(--ctrl-bg-hover);}
  .lightbox-prev{inset-inline-start:4vw;}
  .lightbox-next{inset-inline-end:4vw;}
  @media(max-width:700px){
    .lightbox-nav{width:40px; height:40px; font-size:1.3rem;}
    .lightbox-prev{inset-inline-start:2vw;}
    .lightbox-next{inset-inline-end:2vw;}
  }

  /* ---------- CLIENT LOGO BAR (Stage 4 — reworked) ---------- */
  .logo-strip-section{
    position:relative; padding:52px 0 48px; border-bottom:1px solid var(--line);
  }
  .logo-strip-head{text-align:center; margin-bottom:30px; padding-inline:var(--container-pad);}
  /* Flanked by a rule either side, matching .pg-logobar-title on the
     server-rendered pages — the same bar appears on both and drifted apart
     visually once one of them got the treatment. Capped rather than flex:1
     so they stay hairlines instead of spanning the section. */
  .logo-strip-title{
    display:flex; align-items:center; justify-content:center; gap:16px;
    font-family:var(--font-fa), var(--font-en); font-size:var(--text-base); font-weight:400;
    color:var(--ink-dim); letter-spacing:.02em;
  }
  .logo-strip-title::before, .logo-strip-title::after{
    content:''; flex:1 1 0; max-width:150px; height:1px;
    background:linear-gradient(90deg, transparent, var(--line));
  }
  .logo-strip-title::after{background:linear-gradient(90deg, var(--line), transparent);}
  /* Same reasoning as .pg-logobar-title: too little room left for them to be
     a divider rather than two stray dashes. */
  @media(max-width:600px){
    .logo-strip-title::before, .logo-strip-title::after{display:none;}
  }
  [dir="ltr"] .logo-strip-title{font-family:var(--font-en), var(--font-fa);}
  .logo-strip-row{position:relative; display:flex; align-items:center; min-height:var(--logo-bar-h, 110px);}
  /* The strip is driven by a transform on .logo-track, not by scrollLeft: a
     composited transform can't be knocked out of step by main-thread work, and
     it lets the infinite wrap use exact fractional pixel positions. Hence
     overflow:hidden (no native scroll) + touch-action:pan-y so a vertical
     swipe still scrolls the page while horizontal drags come to our handlers. */
  .logo-marquee{
    width:100%; min-width:0; overflow:hidden; cursor:grab; touch-action:pan-y;
    mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
    -webkit-mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  }
  .logo-marquee.dragging{cursor:grabbing;}
  /* the bar is revealed only once every logo in the first set has loaded and
     been measured, so no visitor ever sees the strip resize itself */
  .logo-track{
    display:flex; align-items:center; width:max-content;
    will-change:transform; backface-visibility:hidden;
    opacity:0; transition:opacity 420ms ease;
  }
  .logo-track.logos-ready{opacity:1;}
  .client-logo-item{
    flex:0 0 auto; height:var(--logo-bar-h, 110px);
    display:grid; place-items:center; padding-inline:var(--logo-gap, clamp(24px, 2.8vw, 46px));
  }
  /* No transform-based sizing here: transform doesn't affect layout, so scaling
     a logo this way made it overlap its neighbours' gaps. Both the global and
     the per-logo scale are folded into the height normalizeLogoImg computes.
     'height' is deliberately absent from the transition list — animating it
     would reflow the whole track for 300ms on every measurement. */
  .client-logo-item img{
    display:block; width:auto; height:auto; max-width:150px; max-height:60px; object-fit:contain;
    filter:var(--logo-mono-filter); opacity:.6;
    transition:opacity var(--transition-normal), transform var(--transition-normal);
  }
  .client-logo-item.color img{filter:none; opacity:.85;}
  .client-logo-item:hover img{opacity:1; transform:scale(1.04);}
  .logo-nav{
    position:absolute; top:50%; transform:translateY(-50%); z-index:2;
    width:44px; height:44px; min-width:44px; min-height:44px; border-radius:50%;
    border:1px solid var(--line); background:var(--bg); color:var(--ink-dim);
    font-size:1.3rem; font-weight:300; display:flex; align-items:center; justify-content:center;
    transition:border-color var(--transition-fast), color var(--transition-fast);
  }
  .logo-nav:hover{border-color:var(--accent); color:var(--accent);}
  .logo-nav:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
  .logo-prev{inset-inline-start:1.5vw;}
  .logo-next{inset-inline-end:1.5vw;}
  @media(max-width:700px){
    .logo-strip-section{padding:36px 0 32px;}
    .logo-strip-head{margin-bottom:20px;}
    .logo-strip-row{min-height:var(--logo-bar-h-mobile, 78px);}
    .client-logo-item{height:var(--logo-bar-h-mobile, 78px); padding-inline:var(--logo-gap-mobile, clamp(16px, 5vw, 28px));}
    .client-logo-item img{max-width:110px; max-height:42px;}
    .logo-nav{display:none;}
  }
  @media (prefers-reduced-motion:reduce){ .logo-marquee{scroll-behavior:auto;} }

  /* ---------- WORLD / CRAFT SECTION ---------- */
  /* registered so the gold border sweep's conic-gradient angle can be
     interpolated by an animation — a plain custom property would jump
     between keyframes instead of rotating smoothly. */
  @property --wc-sweep{ syntax:'<angle>'; initial-value:0deg; inherits:false; }
  /* master strength for the per-card VHS layers: low at rest, pushed up on
     hover. Registered (and inheriting) so it interpolates smoothly and the
     nested ::after layers can read it. */
  @property --wc-vhs{ syntax:'<number>'; initial-value:0.42; inherits:true; }
  /* Ported 1:1 from the "Cinema Toolkit" design file (1180x1080 canvas):
     warm elliptical bloom over a near-black radial base. */
  .world-section{
    position:relative; overflow:hidden;
    --world-glow:.55; --world-grain:1; --world-parallax:1; --world-float-dur:6s;
    background:
      radial-gradient(ellipse 900px 700px at 50% 42%, rgba(120,80,30,.10), transparent 65%),
      radial-gradient(circle at 50% 50%, #100e0b 0%, #080808 70%);
  }
  /* texture pass 1 — coarse + fine film grain plus a low-frequency blotch
     for uneven, mottled density (design: feTurbulence at .85 / 1.6 / .015). */
  .world-section::before{
    content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
    /* a fixed floor plus the admin slider on top: the saved slider value was
       calibrated against the old, much weaker formula, so keying opacity to
       it alone left the grain almost invisible. The floor guarantees a real
       filmic texture; the slider still scales it up from there. */
    opacity:calc(.32 + var(--world-grain, .6) * .55); mix-blend-mode:overlay;
    background-image:
      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.7 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"),
      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='90' height='90'><filter id='n2'><feTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.4 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n2)'/></svg>"),
      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='600'><filter id='n3'><feTurbulence type='fractalNoise' baseFrequency='0.015' numOctaves='3' seed='7' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.6 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n3)'/></svg>");
  }
  /* texture pass 2 — uneven dust blotches (multiply) + a sparse star/dust
     dot field (overlay) + the heavy inset vignette from the design. */
  .world-section::after{
    content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
    box-shadow:inset 0 0 220px 60px rgba(0,0,0,.85);
    background:
      radial-gradient(circle at 6% 12%, rgba(255,255,255,.9) .6px, transparent 1.1px),
      radial-gradient(circle at 21% 38%, rgba(255,255,255,.7) .5px, transparent 1px),
      radial-gradient(circle at 33% 8%, rgba(255,255,255,.85) .7px, transparent 1.2px),
      radial-gradient(circle at 47% 61%, rgba(255,255,255,.6) .5px, transparent 1px),
      radial-gradient(circle at 58% 24%, rgba(255,255,255,.9) .6px, transparent 1.1px),
      radial-gradient(circle at 68% 71%, rgba(255,255,255,.65) .5px, transparent 1px),
      radial-gradient(circle at 77% 15%, rgba(255,255,255,.8) .7px, transparent 1.2px),
      radial-gradient(circle at 84% 46%, rgba(255,255,255,.55) .5px, transparent 1px),
      radial-gradient(circle at 91% 83%, rgba(255,255,255,.9) .6px, transparent 1.1px),
      radial-gradient(circle at 12% 67%, rgba(255,255,255,.6) .5px, transparent 1px),
      radial-gradient(circle at 27% 88%, rgba(255,255,255,.75) .6px, transparent 1.1px),
      radial-gradient(circle at 39% 94%, rgba(255,255,255,.5) .5px, transparent 1px),
      radial-gradient(circle at 53% 9%, rgba(255,255,255,.7) .6px, transparent 1.1px),
      radial-gradient(circle at 63% 52%, rgba(255,255,255,.85) .6px, transparent 1.1px),
      radial-gradient(circle at 72% 34%, rgba(255,255,255,.5) .5px, transparent 1px),
      radial-gradient(circle at 88% 6%, rgba(255,255,255,.65) .5px, transparent 1px),
      radial-gradient(circle at 96% 62%, rgba(255,255,255,.8) .6px, transparent 1.1px),
      radial-gradient(ellipse 260px 90px at 12% 22%, rgba(0,0,0,.4), transparent 70%),
      radial-gradient(ellipse 180px 320px at 88% 70%, rgba(0,0,0,.35), transparent 70%),
      radial-gradient(ellipse 340px 120px at 55% 92%, rgba(0,0,0,.35), transparent 70%),
      radial-gradient(ellipse 220px 160px at 30% 78%, rgba(0,0,0,.3), transparent 72%),
      radial-gradient(ellipse 200px 140px at 70% 15%, rgba(0,0,0,.3), transparent 72%),
      radial-gradient(ellipse 150px 260px at 5% 55%, rgba(0,0,0,.28), transparent 72%);
  }
  .world-section.theme-light{ background:var(--world-light-bg, #f6f1e7); color:#2a2420; }
  /* single centered column at every width: a compact header (eyebrow/title/
     subtitle/CTA), then the card composition below it, full-bleed within the
     container — the equipment cards are the visual focus, not a sidebar
     next to a text column (see layout note in the audit). */
  .world-inner{
    position:relative; z-index:1;
    display:flex; flex-direction:column; align-items:center; gap:44px;
    max-width:var(--container-width); margin-inline:auto;
  }
  /* wider than the old 640px cap so the headline can hold one line; the
     subtitle keeps its own reading measure below. */
  .world-copy{max-width:100%; margin-inline:auto; text-align:center;}
  .world-title{
    font-family:var(--font-fa), var(--font-en); font-weight:600;
    /* fluid so the single line always fits the container instead of wrapping */
    font-size:clamp(1.35rem, 4.1vw, 3.4rem);
    line-height:var(--line-height-tight); margin:0 0 14px; letter-spacing:-.005em;
    white-space:nowrap;
  }
  /* Phone: hold the headline to one line here too. That costs real size — at
     375px the longest of the current headlines only fits on one line at about
     20px, against the 35px a wrapped two-line version could carry — so the
     size is driven off the viewport (capped at 23px, which is what the fluid
     rule above resolves to at 561px, so nothing jumps across the breakpoint).
     The subtitle drops to 70% of its base size to sit under the smaller
     headline without out-weighing it. */
  @media(max-width:560px){ .world-title{ white-space:nowrap; font-size:min(5.2vw, 23px); } }
  [dir="ltr"] .world-title{ font-family:var(--font-en), var(--font-fa); }
  .world-title .accent-word{color:var(--accent); font-weight:600;}
  /* the single small line under the headline — no reading-measure cap, so it
     stays on one line next to the headline instead of stacking into a
     paragraph; it still wraps naturally once the viewport gets narrow. */
  .world-subtitle{
    font-size:var(--text-base); color:var(--ink-dim); font-weight:400;
    line-height:var(--line-height-relaxed); max-width:100%; margin:0 auto;
  }
  /* 70% of base on a phone, to sit under the one-line headline above without
     out-weighing it. Has to come after the base rule, not up with the matching
     .world-title breakpoint — same specificity, so source order decides. */
  @media(max-width:560px){ .world-subtitle{ font-size:calc(var(--text-base) * .7); } }

  /* ---- cinematic headline entrance ----
     Reads like a film title settling into place: the whole line racks from
     out-of-focus into sharp (blur + slight over-scale + wide tracking easing
     shut) while each word rises out of its own clipping mask, then the accent
     words bloom warm. The generic .reveal fade is neutralised here so the
     pieces can be choreographed individually. */
  .world-copy.reveal{ opacity:1; transform:none; transition:none; }
  .wt-word{
    display:inline-block; overflow:hidden; vertical-align:bottom;
    /* room for descenders so the mask never clips a "g" or Persian tail */
    padding-bottom:.16em; margin-bottom:-.16em;
  }
  .wt-word-i{ display:inline-block; }
  .world-title{
    filter:blur(18px); scale:1.06; letter-spacing:.07em;
    transition:filter 1.5s cubic-bezier(.16,1,.3,1),
               scale 1.6s cubic-bezier(.16,1,.3,1),
               letter-spacing 1.6s cubic-bezier(.16,1,.3,1);
  }
  .world-copy.in .world-title{ filter:blur(0); scale:1; letter-spacing:-.005em; }
  .wt-word-i{ transform:translateY(118%) rotate(4deg); opacity:0; }
  .world-copy.in .wt-word-i{
    animation:wtWordRise 1s cubic-bezier(.16,1,.3,1) forwards;
    animation-delay:var(--wt-d, 0ms);
  }
  @keyframes wtWordRise{ to{ transform:translateY(0) rotate(0); opacity:1; } }
  /* the accent words need BOTH animations listed here: this selector is more
     specific than the .wt-word-i rule above, and `animation` is a shorthand,
     so naming only the bloom would drop the rise entirely and leave the gold
     words stuck below their mask, invisible. */
  .world-copy.in .world-title .accent-word{
    animation:wtWordRise 1s cubic-bezier(.16,1,.3,1) var(--wt-d, 0ms) forwards,
              wtAccentBloom 2s ease .85s both;
  }
  /* masks are only needed while the words are travelling — released afterwards
     so the accent bloom isn't clipped by its own word box */
  .world-title.wt-done .wt-word{ overflow:visible; }
  @keyframes wtAccentBloom{
    0%  { text-shadow:0 0 0 rgba(var(--accent-rgb,201,164,104),0); }
    45% { text-shadow:0 0 28px rgba(var(--accent-rgb,201,164,104),.55); }
    100%{ text-shadow:0 0 12px rgba(var(--accent-rgb,201,164,104),.18); }
  }
  /* supporting lines follow the headline in */
  .world-copy .eyebrow{ opacity:0; letter-spacing:.55em; transition:opacity .9s ease, letter-spacing 1.4s cubic-bezier(.16,1,.3,1); }
  .world-copy.in .eyebrow{ opacity:1; letter-spacing:.16em; }
  .world-copy .world-subtitle,
  .world-copy .world-cta-row,
  .world-copy .world-mini-brand{
    opacity:0; transform:translateY(16px);
    transition:opacity .9s ease, transform .9s cubic-bezier(.16,1,.3,1);
  }
  .world-copy.in .world-subtitle{ opacity:1; transform:none; transition-delay:.75s; }
  .world-copy.in .world-cta-row{ opacity:1; transform:none; transition-delay:.9s; }
  .world-copy.in .world-mini-brand{ opacity:.75; transform:none; transition-delay:1.05s; }
  .world-copy .world-rule{ transform:scaleX(0); transition:transform 1.1s cubic-bezier(.16,1,.3,1) .65s; }
  .world-copy.in .world-rule{ transform:scaleX(1); }
  @media(prefers-reduced-motion:reduce){
    .world-title{ filter:none; scale:1; letter-spacing:-.005em; transition:none; }
    .wt-word-i{ transform:none; opacity:1; }
    .world-copy.in .wt-word-i{ animation:none; }
    .world-copy.in .world-title .accent-word{ animation:none; }
    .world-copy .eyebrow{ opacity:1; letter-spacing:.16em; transition:none; }
    .world-copy .world-subtitle, .world-copy .world-cta-row, .world-copy .world-mini-brand{
      opacity:1; transform:none; transition:none;
    }
    .world-copy .world-mini-brand{ opacity:.75; }
    .world-copy .world-rule{ transform:scaleX(1); transition:none; }
  }
  .world-rule{width:56px; height:2px; background:var(--accent); margin:28px auto; opacity:.7;}
  .world-cta{display:inline-flex; align-items:center; gap:10px; box-shadow:0 0 0 rgba(var(--accent-rgb,212,175,110),0); transition:box-shadow var(--transition-normal), background var(--transition-normal), color var(--transition-normal);}
  .world-cta:hover{ box-shadow:0 0 calc(22px * var(--world-glow)) rgba(var(--accent-rgb,212,175,110), calc(.55 * var(--world-glow))); }
  .world-cta-arrow{transition:transform var(--transition-normal);}
  [dir="rtl"] .world-cta-arrow{transform:scaleX(-1);}
  .world-cta:hover .world-cta-arrow{transform:translateX(4px);}
  [dir="rtl"] .world-cta:hover .world-cta-arrow{transform:scaleX(-1) translateX(4px);}

  .world-cta-row{display:flex; align-items:center; justify-content:center; gap:26px; flex-wrap:wrap;}
  .world-cta-secondary{display:inline-flex; align-items:center; gap:8px; color:var(--ink-dim); font-size:var(--text-sm); font-weight:400; transition:color var(--transition-normal);}
  .world-cta-secondary:hover{color:var(--accent);}
  .world-cta-secondary .chev{display:inline-block; transition:transform var(--transition-normal);}
  [dir="rtl"] .world-cta-secondary .chev{transform:scaleX(-1);}
  .world-cta-secondary:hover .chev{transform:translateX(-4px);}
  [dir="rtl"] .world-cta-secondary:hover .chev{transform:scaleX(-1) translateX(-4px);}

  .world-mini-brand{display:flex; align-items:center; justify-content:center; gap:10px; margin:48px auto 0; opacity:.75;}
  .world-mini-brand img{height:20px; width:auto; opacity:.85;}
  .world-mini-brand .wmb-tag{font-family:'IBM Plex Mono', monospace; font-size:var(--text-2xs); letter-spacing:.16em; color:var(--ink-faint); text-transform:uppercase;}

  /* ---- card composition canvas ----
     Desktop (>=1025px): a single bounded, centered canvas (not a sidebar-width
     strip) sized close to the reference composition (~1150x900), with every
     card absolutely placed inside it via --wc-x/--wc-y (% of this box) and
     sized via --wc-w/--wc-h (px) — admin-editable, see the world-cards admin
     tab. Narrower screens fall back to a predictable grid/carousel further
     down; a hand-scattered canvas needs room to breathe that a narrow
     viewport doesn't have. */
  .world-cards-wrap{position:relative; min-height:120px;}
  @media(min-width:1025px){
    .world-cards-wrap{
      width:min(100%, 1180px); margin-inline:auto;
      /* the design file's canvas exactly (1180x1080), so the orbit SVG's
         absolute coordinates line up pixel-for-pixel with the cards'
         percentage positions below */
      aspect-ratio:1180 / 1080;
    }
    .world-cards{ height:100%; }
  }
  .world-cards{ position:relative; z-index:2; }

  /* -- orbit / technical background graphic (design file, 1:1) --
     concentric ellipses + sweeping arcs, nine soft gradient-faded connector
     curves out to each card's anchor dot, and two comets that ride the arcs. */
  .world-orbit-svg{position:absolute; inset:0; width:100%; height:100%; z-index:1; pointer-events:none; overflow:visible;}
  .wo-ring{fill:none; stroke-width:1;}
  .wo-curve{
    fill:none; stroke-width:1.2; stroke-dasharray:1 5;
    opacity:0; transition:opacity 1.4s cubic-bezier(.16,1,.3,1);
  }
  .world-cards-wrap.in-view .wo-curve{opacity:1;}
  .wo-curve:nth-of-type(1){transition-delay:0s;}   .wo-curve:nth-of-type(2){transition-delay:.06s;}
  .wo-curve:nth-of-type(3){transition-delay:.12s;} .wo-curve:nth-of-type(4){transition-delay:.18s;}
  .wo-curve:nth-of-type(5){transition-delay:.24s;} .wo-curve:nth-of-type(6){transition-delay:.3s;}
  .wo-curve:nth-of-type(7){transition-delay:.36s;} .wo-curve:nth-of-type(8){transition-delay:.42s;}
  .wo-curve:nth-of-type(9){transition-delay:.48s;}
  @media (prefers-reduced-motion:reduce){
    .wo-curve{transition:none; opacity:1;}
    .wo-comet{display:none;}
    .wo-pulse{animation:none;}
  }

  .world-card{ position:relative; width:100%; rotate:calc(var(--wc-rotate, 0) * 1deg); }
  @media(min-width:1025px){
    .world-card{
      position:absolute; z-index:2;
      left:var(--wc-x, 50%); top:var(--wc-y, 50%);
      width:var(--wc-w, 220px); height:var(--wc-h, 220px);
      /* --wc-push-* is written by setupWorldHoverSpread() so the other cards
         can drift radially away from whichever one is hovered. */
      translate:calc(-50% + var(--wc-push-x, 0px)) calc(-50% + var(--wc-push-y, 0px));
    }
    /* the generic `.stagger-item` reveal rule further down sets its own
       `transition` and, being later in the sheet at equal specificity, would
       replace this one wholesale — leaving the hover lift with no easing at
       all. The descendant selector outranks it so the smooth easing sticks. */
    .world-cards .world-card{
      transition:translate 820ms cubic-bezier(.16,.84,.28,1),
                 scale 820ms cubic-bezier(.16,.84,.28,1),
                 opacity 620ms ease,
                 --wc-vhs 520ms ease;
    }
    /* hovered card: lifts further and pops a little more than the design's
       resting hover, and its VHS grain ramps up. Uses the standalone
       translate/scale properties (not `transform`) so it composes with the
       card's own `rotate` and never fights the entrance animation. */
    .world-card:hover{
      z-index:9;
      translate:calc(-50% + var(--wc-push-x, 0px)) calc(-50% - 12px + var(--wc-push-y, 0px));
      scale:calc(var(--wc-scale, 1) * 1.055);
      --wc-vhs:1.25;
    }
    /* everything else recedes: nudged outward by the JS push, scaled down a
       touch and dimmed, with its grain settling back. */
    .world-cards:has(.world-card:hover) .world-card:not(:hover){
      scale:calc(var(--wc-scale, 1) * .972);
      opacity:.8;
      --wc-vhs:.3;
    }
    /* snow flickers faster while hovered — reads as the tape struggling */
    .world-card:hover .world-card-inner::after{ animation-duration:.28s; }
  }

  /* the design composition is static — no bob/float. Kept as a structural
     wrapper so the markup and the admin preview stay in sync. */
  .world-card-float{ position:relative; height:100%; }

  .world-card-inner{ position:relative; z-index:1; display:block; height:100%; color:inherit; text-decoration:none; }

  /* ---- VHS texture pass, per card ----
     The section grain sits BEHIND the cards, and the plate is near-opaque, so
     the tiles themselves read clean. This layer puts the noise back on top of
     each card: flickering static snow + tight scanlines, clipped to the card's
     rounded rect so it reads like the tile is a VHS frame. Strength follows the
     same admin Grain slider as the background, so one control drives both. */
  /* 1) flickering static snow. Desaturated to grey in the filter (raw
     feTurbulence is colourful, which reads as confetti rather than static)
     and blended with `screen` so it actually lifts off the near-black plate —
     `overlay` is almost invisible against this dark a surface. */
  .world-card-inner::after{
    content:''; position:absolute; inset:0; z-index:4; pointer-events:none;
    border-radius:20px;
    opacity:calc((.3 + var(--world-grain, .6) * .35) * var(--wc-vhs, .42));
    mix-blend-mode:screen;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='150' height='150'><filter id='v'><feTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncA type='linear' slope='0.55'/></feComponentTransfer></filter><rect width='100%25' height='100%25' filter='url(%23v)'/></svg>");
    background-size:150px 150px;
    animation:wcVhsSnow .45s steps(4) infinite;
  }
  @keyframes wcVhsSnow{
    0%  { background-position:0      0; }
    25% { background-position:-37px  23px; }
    50% { background-position: 29px -41px; }
    75% { background-position:-53px -17px; }
    100%{ background-position: 17px  47px; }
  }
  /* 2) tight scanlines — plain dark stripes, normal blend so they stay visible
     (screen would erase them, since screen ignores black). */
  .world-card::after{
    content:''; position:absolute; inset:0; z-index:6; pointer-events:none;
    border-radius:20px;
    opacity:calc((.4 + var(--world-grain, .6) * .35) * var(--wc-vhs, .42));
    background-image:repeating-linear-gradient(0deg,
      rgba(0,0,0,.42) 0 1px, rgba(255,255,255,.045) 1px 2px, transparent 2px 4px);
  }
  /* 3) the VHS head-switching tell: a soft tracking band drifting down */
  .world-card-float::after{
    content:''; position:absolute; inset:0; z-index:5; pointer-events:none;
    border-radius:20px;
    opacity:calc((.35 + var(--world-grain, .6) * .3) * var(--wc-vhs, .42));
    mix-blend-mode:screen;
    background:linear-gradient(180deg,
      transparent 0%, rgba(255,255,255,.10) 40%, rgba(255,255,255,.2) 50%,
      rgba(255,255,255,.10) 60%, transparent 100%);
    background-size:100% 22%; background-repeat:no-repeat;
    animation:wcVhsTrack 6.5s linear infinite;
  }
  @keyframes wcVhsTrack{
    0%  { background-position:0 -25%; }
    100%{ background-position:0 125%; }
  }
  @media(prefers-reduced-motion:reduce){
    .world-card-inner::after{ animation:none; }
    .world-card-float::after{ animation:none; opacity:0; }
  }

  /* design card plate: translucent charcoal with a warm corner bloom, thin
     warm border, deep drop shadow + a 1px inner top highlight. */
  .world-card-tilt{
    position:absolute; inset:0; z-index:0;
    border:1px solid rgba(110,92,68,.22);
    border-radius:20px;
    background:
      radial-gradient(75px 58px at var(--wc-corner, 0% 0%), rgba(201,150,80,.15), transparent 55%),
      rgba(13,13,12,.88);
    box-shadow:0 18px 50px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,220,170,.035);
    transition:border-color 820ms cubic-bezier(.16,.84,.28,1), box-shadow 820ms cubic-bezier(.16,.84,.28,1);
  }
  .world-card:hover .world-card-tilt{
    border-color:rgba(214,164,96,.6);
    box-shadow:0 22px 55px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,220,170,.06);
  }
  /* the single warm accent hairline on one edge of each card — position,
     size and gradient direction are per-card presets from the design. On
     hover it hands off to the travelling sweep below, so it reads as the
     same gold bar detaching and running around the card. */
  .world-card-tilt::after{
    content:''; position:absolute; pointer-events:none;
    top:var(--wc-line-top, auto); right:var(--wc-line-right, auto);
    bottom:var(--wc-line-bottom, auto); left:var(--wc-line-left, auto);
    width:var(--wc-line-w, 60px); height:var(--wc-line-h, 2px);
    background:var(--wc-line-bg, linear-gradient(90deg, transparent, rgba(255,195,120,.75), transparent));
    transition:opacity 450ms ease;
  }
  .world-card:hover .world-card-tilt::after{ opacity:0; }

  /* travelling gold sweep: a rotating conic-gradient clipped to a 1px ring
     on the card's outline via the two-layer mask/exclude trick, so only the
     border shows the moving highlight. Needs the registered --wc-sweep angle
     below to be animatable; where @property is unsupported the gradient just
     sits still, which degrades to a static warm edge rather than breaking. */
  .world-card-tilt::before{
    content:''; position:absolute; inset:-1px; border-radius:inherit;
    padding:1px; pointer-events:none; opacity:0;
    background:conic-gradient(from var(--wc-sweep, 0deg),
      transparent 0deg, rgba(255,203,133,.95) 16deg, rgba(255,203,133,.35) 34deg, transparent 62deg);
    -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite:xor; mask-composite:exclude;
    transition:opacity 450ms ease;
  }
  .world-card:hover .world-card-tilt::before{
    opacity:1; animation:wcSweep 2.8s linear infinite;
  }
  @keyframes wcSweep{ to{ --wc-sweep:360deg; } }
  /* per-card corner-bloom origin + accent-line placement (design file) */
  .world-card[data-icon="camera"]{ --wc-corner:0% 0%;
    --wc-line-top:-1px; --wc-line-right:30px; --wc-line-w:60px; --wc-line-h:2px; }
  .world-card[data-icon="lens"]{ --wc-corner:100% 0%;
    --wc-line-bottom:14px; --wc-line-left:-1px; --wc-line-w:2px; --wc-line-h:54px;
    --wc-line-bg:linear-gradient(180deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="gimbal"]{ --wc-corner:100% 0%;
    --wc-line-top:40%; --wc-line-right:-1px; --wc-line-w:2px; --wc-line-h:50px;
    --wc-line-bg:linear-gradient(180deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="fpv"]{ --wc-corner:0% 100%;
    --wc-line-bottom:30px; --wc-line-left:30px; --wc-line-w:54px; --wc-line-h:2px;
    --wc-line-bg:linear-gradient(90deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="clapboard"]{ --wc-corner:0% 0%;
    --wc-line-top:16px; --wc-line-right:-1px; --wc-line-w:2px; --wc-line-h:44px;
    --wc-line-bg:linear-gradient(180deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="edit"]{ --wc-corner:100% 0%;
    --wc-line-bottom:-1px; --wc-line-right:40px; --wc-line-w:58px; --wc-line-h:2px;
    --wc-line-bg:linear-gradient(90deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="color"]{ --wc-corner:0% 0%;
    --wc-line-top:-1px; --wc-line-left:24px; --wc-line-w:54px; --wc-line-h:2px;
    --wc-line-bg:linear-gradient(90deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="audio"]{ --wc-corner:100% 0%;
    --wc-line-bottom:20px; --wc-line-right:-1px; --wc-line-w:2px; --wc-line-h:44px;
    --wc-line-bg:linear-gradient(180deg, transparent, rgba(255,195,120,.7), transparent); }
  .world-card[data-icon="software"]{ --wc-corner:100% 100%;
    --wc-line-top:24px; --wc-line-left:-1px; --wc-line-w:2px; --wc-line-h:48px;
    --wc-line-bg:linear-gradient(180deg, transparent, rgba(255,195,120,.7), transparent); }
  /* design entrance: each card fades up 20px, staggered 70ms apart, keeping
     its own resting rotation (which lives on the outer .world-card via the
     standalone `rotate` property, so it composes rather than conflicts). */
  .world-card.stagger-item .world-card-inner{ opacity:0; transform:translateY(20px); }
  /* keyframe (not transition): a plain `transition` here would also govern
     the hover border/shadow change forever after (since .stagger-item is
     never removed), quietly making hover feel delayed. A one-shot animation
     avoids that entirely. */
  .world-card.stagger-item.in .world-card-inner{
    animation:worldCardRiseIn 600ms cubic-bezier(.22,.61,.36,1) forwards;
    animation-delay:var(--stagger-delay, 0ms);
  }
  @keyframes worldCardRiseIn{ to{ transform:translateY(0); opacity:1; } }

  /* card content column, design-exact: 22px padding, title then English
     subtitle then the media area filling the rest. Both text lines are
     LEFT-aligned even in the RTL layout — that's the design's deliberate
     look, not an RTL bug. */
  .world-card-content{
    position:relative; z-index:2; box-sizing:border-box;
    padding:18px;
  }
  .world-card-title{
    direction:rtl; text-align:left;
    font-family:var(--font-fa), var(--font-en); font-weight:500; font-size:17px;
    color:rgba(238,226,208,.88); line-height:1.25;
  }
  [dir="ltr"] .world-card-title{ font-family:var(--font-en), var(--font-fa); }
  .world-card-subtitle{
    text-align:left; margin-top:3px;
    font-family:var(--font-en), sans-serif; font-size:12px; font-weight:400;
    color:rgba(208,194,174,.55); line-height:1.35; letter-spacing:0;
  }
  /* The card is a fixed-height box (--wc-h): the content column fills it and
     the media area takes whatever the caption leaves. Unconditional on
     purpose — the narrow-screen column further down keeps the same authored
     --wc-w/--wc-h box, so the card FORM stays identical to desktop and only
     the LAYOUT around it changes. */
  .world-card-content{ height:100%; display:flex; flex-direction:column; padding:22px; }
  .world-card-title{font-size:19px;}
  .world-card-subtitle{font-size:13px;}

  /* mobile/tablet fallback shape only — a plain contained thumbnail, since
     the desktop breakout below needs a fixed card height to hang off of */
  .world-card--lg .world-card-media{aspect-ratio:4/5;}
  .world-card--md .world-card-media{aspect-ratio:3/2;}
  .world-card--sm .world-card-media{aspect-ratio:16/10;}
  .world-card-media{
    position:relative; z-index:1; overflow:hidden; border-radius:10px;
    display:flex; align-items:center; justify-content:center;
    margin-top:10px;
  }
  .world-card-media-inner{ position:absolute; inset:0; }
  .world-card-media img{
    width:100%; height:100%; object-fit:contain;
    opacity:0; transition:opacity .5s ease;
  }
  /* the design's two-line English captions come through as real newlines in
     the DB subtitle, so the wrap point matches the design exactly instead of
     depending on where the box happens to break */
  .world-card-subtitle{ white-space:pre-line; }
  .world-card-media img.img-loaded{opacity:1;}
  .world-card-media .world-card-icon{
    width:38%; max-width:64px; color:var(--ink-faint); opacity:.85;
  }

  /* THE design's signature move: the product cutout is NOT boxed inside the
     card — it's an oversized layer hung off the media area with negative
     insets, so it deliberately spills past the plate's edges, with its own
     drop shadow so it reads as floating above the card.
     The .world-card--lg/md/sm aspect-ratio rules above are overridden here at
     every width; they only still bite if a card is ever rendered without the
     fixed --wc-h box (the admin preview supplies its own height). */
  .world-card-media{ flex:1; aspect-ratio:auto; overflow:visible; border-radius:0; background:none; min-height:0; }
  .world-card-title, .world-card-subtitle{ flex:0 0 auto; }
  .world-card-media-inner{
    position:absolute; inset:var(--wc-media-inset, -25% -40% -50% -20%);
    filter:drop-shadow(0 18px 24px rgba(0,0,0,.55));
    transform:translate(var(--wc-media-x, 10%), var(--wc-media-y, 5%)) scale(var(--wc-media-scale, 1));
    transition:transform 820ms cubic-bezier(.16,.84,.28,1);
  }
  /* two nested zooms, exactly like the design tool: the outer one is the
     card's authored media scale, the inner one is the per-image zoom the
     designer set inside that image slot (kept admin-editable as image_scale). */
  .world-card-media img{ transform:scale(var(--wc-img-scale, 1)); transition:transform 820ms cubic-bezier(.16,.84,.28,1); }
  .world-card:hover .world-card-media img{ transform:scale(calc(var(--wc-img-scale, 1) * 1.04)); }
  /* per-card breakout box + resting offset/zoom, straight from the design */
  .world-card[data-icon="camera"]{    --wc-media-inset:-35% -55% -55% -20%; --wc-media-x:10%; --wc-media-y:5%; --wc-media-scale:1; }
  .world-card[data-icon="lens"]{      --wc-media-inset:-25% -40% -60% -30%; --wc-media-x:12%; --wc-media-y:8%; --wc-media-scale:1.05; }
  .world-card[data-icon="gimbal"]{    --wc-media-inset:-25% -45% -60% -25%; --wc-media-x:12%; --wc-media-y:12%; --wc-media-scale:1.05; }
  .world-card[data-icon="fpv"]{       --wc-media-inset:-45% -30% -35% -30%; --wc-media-x:0%;  --wc-media-y:-5%; --wc-media-scale:1.1; }
  .world-card[data-icon="clapboard"]{ --wc-media-inset:-30% -45% -50% -25%; --wc-media-x:10%; --wc-media-y:8%; --wc-media-scale:1.05; }
  .world-card[data-icon="edit"]{      --wc-media-inset:-25% -40% -45% -18%; --wc-media-x:10%; --wc-media-y:5%; --wc-media-scale:1.02; }
  .world-card[data-icon="color"]{     --wc-media-inset:-25% -40% -50% -20%; --wc-media-x:10%; --wc-media-y:10%; --wc-media-scale:1.05; }
  .world-card[data-icon="audio"]{     --wc-media-inset:-25% -40% -45% -18%; --wc-media-x:10%; --wc-media-y:5%; --wc-media-scale:1.03; }
  .world-card[data-icon="software"]{  --wc-media-inset:-20% -25% -30% -15%; --wc-media-x:0%;  --wc-media-y:0%; --wc-media-scale:1; }
  /* the FPV card is the one that flips the stack: drone image on top,
     caption underneath (design file, "FPV" block) */
  .world-card[data-icon="fpv"] .world-card-media{ order:-1; margin-top:0; }
  .world-card[data-icon="fpv"] .world-card-title{ margin-top:10px; }

  /* software-tools mini grid card (matches the reference's colorful 3x2 icon tile) */
  .world-card-tools{display:grid; grid-template-columns:repeat(3,1fr); gap:10px; width:100%; height:100%;}
  .world-card-tools .tool-tile{
    border-radius:10px; display:flex; align-items:center; justify-content:center;
  }

  @media(prefers-reduced-motion:reduce){
    .world-card-float{animation:none;}
    .world-card-tilt{transition:border-color var(--transition-normal), box-shadow var(--transition-normal) !important;}
    .world-card.stagger-item.in .world-card-inner{animation:none; opacity:1; transform:none;}
    /* keep the gold edge, drop the travelling rotation */
    .world-card:hover .world-card-tilt::before{animation:none;}
    .world-card:hover .world-card-tilt::after{opacity:1;}
  }

  /* light theme tuning: not just an inverted dark palette — warmer, softer */
  :root[data-theme="light"] .world-card-tilt, html:not([data-theme="dark"]) .world-section.theme-follows-site .world-card-tilt{
    background:linear-gradient(160deg, #fffdf8, #f3ece0);
    box-shadow:0 18px 40px -24px rgba(120,90,40,.22);
  }
  :root[data-theme="light"] .wo-curve, :root[data-theme="light"] .wo-ring{ stroke:#b08d4f; }
  :root[data-theme="light"] .world-card:hover .world-card-tilt{ border-color:#a97f3a; }

  /* ---------- WORLD CARDS — NARROW SCREENS: THE CANVAS, SCALED ----------
     Deliberately NOT a responsive re-flow. The composition is kept whole —
     same 1180x1080 canvas, same nine scattered positions, same orbit curves —
     and simply shrunk to whatever width there is. On a phone the section is a
     picture of the kit rather than a list to read: the captions do end up
     small, and that is the accepted trade for keeping the arrangement.

     Everything below just re-states the desktop canvas rules (which are gated
     at min-width:1025px so the desktop path stays byte-identical) and hands
     the scaling to `zoom` on the wrap. `zoom` scales the layout box too, so
     the shrunken canvas reserves exactly the height it draws — no transform
     gap to compensate for — and it takes the whole card with it: type,
     padding, radii, shadows and the spilling artwork all keep their authored
     proportions, which a percentage-based re-size never would.

     --world-canvas-zoom is written by setupWorldCanvasFit(). It can't be
     expressed in CSS: `zoom` needs a plain number and calc() cannot divide a
     length by a length. The fallback below is the ~375px phone case, so the
     canvas is still sane for the frame before the script measures.

     Replaces two older fallbacks — a 2-column grid and a snap carousel. That
     carousel never actually scrolled: .world-cards-wrap is a flex item of a
     centred column-flex parent, so with no width of its own it grew to the
     full row width (2452px on a 375px screen), leaving scrollWidth ==
     clientWidth and six of the nine cards permanently off-screen. The fixed
     width below is what keeps that class of bug from coming back. */
  @media(max-width:1024px){
    .world-cards-wrap{
      width:1180px; aspect-ratio:1180 / 1080; margin-inline:auto;
      zoom:var(--world-canvas-zoom, .2797);
    }
    .world-cards{ height:100%; }
    .world-card{
      position:absolute; z-index:2;
      left:var(--wc-x, 50%); top:var(--wc-y, 50%);
      width:var(--wc-w, 220px); height:var(--wc-h, 220px);
      translate:-50% -50%;
    }
  }

  /* ---------- WORLD CARDS — PHONE: COMPACT TWO-COLUMN GRID ----------
     Chosen over the scaled canvas above, which survives on a tablet (~0.66,
     captions ~12px) but not on a phone: at 375px it renders the canvas at
     0.28, putting the titles at ~5px and turning a section that names nine
     tools into pure decoration.

     So below 700px the composition is dropped rather than shrunk. All nine
     cards stay in one screenful, and the type is sized for the smaller card
     instead of being inherited from the desktop canvas.

     The product cutout is pulled back INSIDE the plate here. The breakout is
     the design's signature, but it hangs up to ~55% of the media box past the
     card edge and needs the canvas's empty space around each card to read as
     floating — in a 2-up grid it would just collide with the neighbour and
     get clipped, so a contained thumbnail is the honest call at this width.

     Ordering matters: this block must stay AFTER the max-width:1024px canvas
     rules, since both are class-specificity and source order is what decides.
     (--world-canvas-zoom is still written by setupWorldCanvasFit() below this
     breakpoint; zoom:1 here makes it inert rather than needing a JS guard.) */
  @media(max-width:700px){
    .world-cards-wrap{ width:100%; aspect-ratio:auto; zoom:1; min-height:0; }
    .world-cards{
      display:grid; grid-template-columns:1fr 1fr; gap:12px; height:auto;
    }
    /* Nine cards leave an orphan on a 2-up grid. The tools tile is the one
       that reads as a bonus rather than a piece of kit, so it sits this
       breakpoint out and the grid closes at a clean 4x2.
       Note this is keyed to the card, not to its position: if cards are ever
       added or reordered in the admin, re-check which one ends up odd. */
    .world-card[data-icon="software"]{ display:none; }
    .world-card{
      position:relative; left:auto; top:auto;
      width:100%; height:100%;
      translate:none; rotate:none;
    }
    .world-card-content{ height:100%; padding:14px; }
    .world-card-title{ font-size:15px; }
    /* two lines reserved whether the caption fills them or not, so the media
       area starts at the same height in every tile (some subtitles are one
       line, some carry a real newline from the DB and wrap to two) */
    .world-card-subtitle{ font-size:11px; min-height:2.7em; }
    /* The per-size aspect ratios (--lg 4/5, --md 3/2, --sm 16/10) are exactly
       what made the thumbnails differ, so all three are collapsed to one ratio
       here — they need the same specificity to be beaten.
       A definite ratio rather than flex:1 on purpose: the card has no height
       of its own in this grid, so a media box that only fills leftover space
       resolves to zero and the thumbnail disappears. With the ratio fixed and
       the caption block reserved below, every tile computes to the same height
       on its own, and it scales with the column instead of a magic pixel size. */
    /* Square rather than the old 4/3. The source cutouts are all square PNGs,
       so in a 4/3 box `contain` sized them off the SHORT edge and the picture
       could never be taller than the box — the tile spent a third of its width
       on empty side margin and still rendered a small product. A square box
       spends that width on the image instead, which is what buys both the
       larger resting size and the room for a deeper zoom below. */
    .world-card--lg .world-card-media,
    .world-card--md .world-card-media,
    .world-card--sm .world-card-media,
    .world-card-media{
      overflow:hidden; border-radius:10px;
      flex:none; aspect-ratio:1/1; min-height:0; margin-top:10px;
    }
    /* The vertical inset is what sets the resting size (the square image is
       sized off the inner box's short edge, which is its height): 10% here
       leaves the cutout ~20% larger than the old 4/3 box gave, and still keeps
       ~7px of clearance at the bottom of the zoom below, so nothing crops. */
    .world-card-media-inner{ position:absolute; inset:10% 3%; transform:none; filter:none; }
    /* The FPV card flips its stack on desktop (drone above the caption, per the
       design file). In the 2-up grid that makes it the one tile whose title sits
       bottom-left while all eight others read top-left, so the flip is undone
       here and the drone goes back under the caption like every other card. */
    .world-card[data-icon="fpv"] .world-card-media{ order:0; margin-top:10px; }
    .world-card[data-icon="fpv"] .world-card-title{ margin-top:0; }
    /* The admin's per-image zoom (--wc-img-scale) exists to frame each cutout as
       it breaks OUT of the plate on desktop — up to 1.3 on the lens. Reused
       inside a contained thumbnail it only crops the product against the box,
       and it made hover wildly uneven card to card: invisible at 0.97 on the
       drone, a hard crop at 1.3 on the lens. So the phone grid ignores it and
       every card gets the same gentle, fully-contained zoom instead. */
    .world-card-media img{ transform:none; }
    .world-card:hover .world-card-media img{ transform:scale(1.18); }
    /* the orbit's curves are authored against the 1180x1080 canvas and anchor
       to each card's scattered position — in a grid they'd point at nothing */
    .world-orbit-svg{ display:none; }
  }

  /* ---------- STAGGERED REVEAL ---------- */
  .stagger-item{opacity:0; transform:translateY(18px); transition:opacity .6s ease, transform .6s ease;}
  .stagger-item.in{opacity:1; transform:translateY(0);}

  /* ---------- SERVICES ---------- */
  .services-list{margin-top:50px;}
  .service-row{
    display:flex; align-items:baseline; gap:28px; padding:32px 0; border-top:1px solid var(--line);
    transition:padding-inline-start .3s, border-color .3s;
  }
  .services-list .service-row:last-child{border-bottom:1px solid var(--line);}
  .service-row:hover{padding-inline-start:16px; border-color:var(--accent);}
  .service-num{font-family:'IBM Plex Mono'; color:var(--ink-faint); font-size:var(--text-2xs); min-width:34px; flex-shrink:0;}
  .service-title{font-size:var(--heading-card); line-height:1.3; font-weight:400; min-width:230px; flex-shrink:0;}
  .service-desc{color:var(--ink-dim); font-size:var(--text-base); font-weight:400; line-height:var(--line-height-relaxed); flex:1;}
  /* Rows that lead to a dedicated service page. Same row, made clickable —
     the arrow is the only visual difference, so the section keeps reading as
     one list rather than splitting into "links" and "not links". */
  a.service-row{text-decoration:none; color:inherit;}
  a.service-row .service-title{color:var(--ink);}
  a.service-row:hover .service-title{color:var(--accent);}
  .service-go{color:var(--accent); font-size:var(--text-xs); flex-shrink:0; opacity:0; transform:translateX(6px); transition:opacity .3s, transform .3s;}
  a.service-row:hover .service-go{opacity:1; transform:none;}
  /* The three-column row needs 34 + 28 + 230 + 28 = 320px before the
     description gets anything, and both the number and the title are
     flex-shrink:0 — so on a 375px phone (330px of content) the description was
     squeezed to 50px and pushed out of frame, one or two words per line, for
     every row. Wrap it onto its own full-width line instead. */
  @media(max-width:700px){
    .service-row{flex-wrap:wrap; gap:0 14px; padding:26px 0;}
    .service-title{min-width:0; flex:1;}
    .service-desc{flex:0 0 100%; margin-top:10px;}
  }

  /* ---------- PROCESS ---------- */
  .process-list{margin-top:50px;}
  .process-item{display:grid; grid-template-columns:70px 1fr; gap:24px; padding:26px 0; border-top:1px solid var(--line);}
  .process-list .process-item:last-child{border-bottom:1px solid var(--line);}
  .process-num{font-family:'IBM Plex Mono'; color:var(--accent); font-size:1.15rem;}
  .process-title{font-weight:400; font-size:var(--heading-card); line-height:1.3; margin-bottom:8px;}
  .process-desc{color:var(--ink-dim); font-size:var(--text-base); font-weight:400; line-height:var(--line-height-relaxed);}

  /* ---------- TESTIMONIALS ---------- */
  .testimonials-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(300px,1fr)); gap:24px; margin-top:50px;}
  .testimonial-card{border:1px solid var(--line); border-radius:20px; padding:32px; background:var(--bg-elev);}
  .testimonial-quote{font-size:var(--text-lg); font-weight:400; color:var(--ink); margin-bottom:22px; line-height:var(--line-height-relaxed);}
  .testimonial-quote::before{content:'"'; color:var(--accent); font-size:1.5rem; margin-inline-end:2px;}
  .testimonial-name{font-weight:500; font-size:var(--text-base);}
  .testimonial-role{font-family:'IBM Plex Mono'; font-size:var(--text-2xs); color:var(--ink-faint); margin-top:3px; letter-spacing:.04em;}

  /* ---------- CITIES ---------- */
  .cities-chips{display:flex; flex-wrap:wrap; gap:12px; margin-top:40px;}
  .city-chip{
    padding:10px 22px; border:1px solid var(--line); border-radius:999px;
    font-size:var(--text-sm); color:var(--ink-dim); background:var(--bg-elev);
  }

  /* ---------- MAGAZINE ---------- */
  .magazine-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(260px,1fr)); gap:20px; margin-top:50px;}
  .magazine-card{display:block; border:1px solid var(--line); border-radius:16px; padding:26px; transition:border-color .25s, transform .25s;}
  .magazine-card:hover{border-color:var(--accent); transform:translateY(-3px);}
  .magazine-title{font-weight:400; font-size:var(--heading-card); line-height:1.3; margin-bottom:10px;}
  .magazine-link-label{font-family:'IBM Plex Mono'; font-size:var(--text-2xs); color:var(--accent); letter-spacing:.06em; text-transform:uppercase;}

  /* ---------- FAQ ---------- */
  .faq-list{margin-top:50px; max-width:820px; margin-inline:auto;}
  .faq-item{border-top:1px solid var(--line);}
  .faq-list .faq-item:last-child{border-bottom:1px solid var(--line);}
  .faq-q{
    width:100%; text-align:start; padding:22px 0; display:flex; justify-content:space-between; align-items:center;
    font-size:var(--text-base); font-weight:400; color:var(--ink); cursor:pointer; gap:20px; background:none; border:none; font-family:inherit;
  }
  .faq-q .faq-icon{font-family:'IBM Plex Mono'; color:var(--accent); font-size:1.3rem; flex-shrink:0; transition:transform .3s; line-height:1;}
  .faq-item.open .faq-icon{transform:rotate(45deg);}
  .faq-a{max-height:0; overflow:hidden; transition:max-height .4s ease;}
  .faq-a p{color:var(--ink-dim); font-size:var(--text-base); font-weight:400; line-height:var(--line-height-relaxed); padding-bottom:22px; max-width:70ch;}
  .faq-item.open .faq-a{max-height:300px;}

  /* ---------- CONTACT (compact rounded card) ---------- */
  /* 720px, up from 600px, is what the two-column contact grid below actually
     needs: the widest card (the email address, beside its icon and label) has a
     content width of 310px, so 310*2 + 12 gap + 88 card padding = 720. At 600px
     the grid could not fit and spilled ~90px outside the card's rounded border. */
  .contact-card{
    max-width:720px; margin:0 auto; padding:52px 44px; border:1px solid var(--line); border-radius:28px;
    background:linear-gradient(180deg, var(--bg-elev), var(--bg));
  }
  .contact-card p#contactLede{color:var(--ink-dim); font-weight:400; max-width:44ch; margin:0 auto; font-size:var(--text-base); line-height:var(--line-height-relaxed);}
  /* Single column is the default and the narrow-screen layout. Two-up is opted
     into further down, only at a width where a full email address genuinely
     fits beside its icon and label — below that, two columns can only either
     overflow the card or ellipsise the contact details, and a truncated phone
     number is worse than a taller list. */
  .contact-links{display:grid; grid-template-columns:1fr; gap:12px; margin-bottom:36px;}

  /* ---------- CONTACT FORM STEPS ---------- */
  .cf-steps{display:flex; align-items:center; justify-content:center; gap:10px; margin-bottom:28px;}
  .cf-step{display:flex; align-items:center; gap:8px; font-size:var(--text-sm); color:var(--ink-faint); font-weight:500;}
  .cf-step.active{color:var(--accent);}
  .cf-step.done{color:var(--ink-dim);}
  .cf-step-num{
    width:24px; height:24px; border-radius:50%; border:1px solid var(--line); display:flex; align-items:center; justify-content:center;
    font-size:var(--text-2xs); flex-shrink:0;
  }
  .cf-step.active .cf-step-num{border-color:var(--accent); color:var(--accent);}
  .cf-step-line{width:36px; height:1px; background:var(--line);}
  .cf-panel{transition:opacity .3s ease;}
  .field-error{color:#e5484d; font-size:var(--text-2xs); margin-top:6px; min-height:1em;}
  .field.has-error input, .field.has-error textarea{border-color:#e5484d;}
  .cf-panel2-actions{display:flex; gap:12px;}
  .cf-panel2-actions .btn{flex:1; justify-content:center;}
  .contact-link{
    display:flex; align-items:center; gap:14px; padding:14px 18px;
    border:1px solid var(--line); border-radius:16px; background:var(--bg);
    transition:border-color .25s, transform .25s, box-shadow .25s;
  }
  .contact-link:hover{border-color:var(--accent); transform:translateY(-2px); box-shadow:var(--shadow-soft);}
  .contact-link .cl-ico{
    width:38px; height:38px; border-radius:12px; flex-shrink:0; display:flex; align-items:center; justify-content:center;
    border:1px solid var(--line); color:var(--accent);
  }
  .contact-link .cl-ico svg{width:17px; height:17px;}
  /* Icon and label sit at the reading edge, the value is pushed to the opposite
     edge. Stacking the value under the label (the old layout) piled icon, label
     and value all against the right in RTL and left the left half of every card
     empty; splitting them balances the row and gives the Latin value its own
     LTR run instead of an RTL line that happens to contain digits. */
  .contact-link .cl-label{font-size:var(--text-sm); color:var(--ink-dim); flex-shrink:0;}
  /* flex:1 rather than margin-inline-start:auto — logical margins resolve
     against the element's OWN direction, and this element is forced to ltr for
     the Latin value, so an auto inline-start margin here pushes it right (back
     into the label) on the RTL page instead of out to the far edge. Letting it
     take the leftover space and aligning the text inside is direction-safe. */
  .contact-link .cl-value{
    flex:1; min-width:0; direction:ltr; unicode-bidi:isolate; text-align:start;
    font-family:var(--font-en), var(--font-fa); font-size:var(--text-sm); font-weight:400;
    color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  /* on the English page the value belongs at the right-hand edge instead */
  [dir="ltr"] .contact-link .cl-value{text-align:end;}
  /* Desktop: two columns, minmax(0,·) so both tracks stay exactly even. A bare
     1fr is minmax(auto, 1fr), and that auto floor is the card's min-content
     width — which is what let the email column grow to 310px against the
     phone's 277px and push the whole grid outside the card. */
  @media(min-width:860px){
    .contact-links{grid-template-columns:repeat(2, minmax(0, 1fr));}
    /* An odd number of contact methods leaves the last card alone on its row,
       hard against one edge. Centre it across both columns at exactly one
       column's width so the block still reads as a tidy two-up.
       :last-child:nth-child(odd) matches only when the total count IS odd — on
       an even list nothing here applies. */
    .contact-link:last-child:nth-child(odd){
      grid-column:1 / -1; width:calc(50% - 6px); margin-inline:auto;
    }
  }
  .contact-card .field label{
    font-family:var(--font-fa), var(--font-en); font-size:var(--text-sm); font-weight:500;
    letter-spacing:0; text-transform:none; color:var(--ink-dim);
  }
  .contact-card .field input, .contact-card .field textarea{font-size:var(--text-base);}
  .contact-card #cfSubmit{min-height:54px; font-size:var(--text-base); font-weight:600;}
  .field{margin-bottom:22px;}
  .field label{display:block; font-family:var(--font-mono); font-size:var(--text-sm); letter-spacing:.1em; color:var(--ink-faint); text-transform:uppercase; margin-bottom:10px;}
  .field input, .field textarea, .field select{
    width:100%; background:transparent; border:none; border-bottom:1px solid var(--line); color:var(--ink); padding:8px 2px;
    font-family:var(--font-fa); font-weight:400; font-size:var(--text-base); transition:border-color .25s;
  }
  .field input, .field select{min-height:46px;}
  .field select{background:var(--bg); cursor:pointer;}
  .contact-card .field input, .contact-card .field textarea{
    border:1px solid var(--line); border-radius:14px; padding:12px 16px; background:var(--bg);
  }
  .field input:focus, .field textarea:focus, .field select:focus{border-color:var(--accent);}
  .field textarea{min-height:90px; resize:vertical;}
  .btn{
    display:inline-flex; align-items:center; gap:12px; background:transparent; color:var(--ink); border:1px solid var(--ink);
    padding:14px 30px; min-height:48px; font-weight:400; font-size:var(--text-sm); letter-spacing:.06em; transition:all .3s; border-radius:14px;
  }
  .btn:hover{background:var(--ink); color:var(--bg);}
  .btn.ghost{border-color:var(--line); color:var(--ink-dim);}
  .btn.ghost:hover{border-color:var(--accent); color:var(--accent); background:transparent;}
  .btn.primary{border-color:var(--accent); color:var(--accent);}
  .btn.primary:hover{background:var(--accent); color:var(--bg);}
  .form-msg{font-family:'IBM Plex Mono'; font-size:var(--text-2xs); margin-top:18px; color:var(--accent);}

  footer.site-footer{
    padding-block:70px 0;
    padding-inline:max(var(--container-pad), calc((100% - var(--container-width)) / 2));
    border-top:1px solid var(--line);
    font-family:var(--font-fa), var(--font-en); color:var(--ink-dim); font-size:var(--text-sm);
  }
  [dir="ltr"] footer.site-footer{font-family:var(--font-en), var(--font-fa);}
  .footer-grid{display:grid; grid-template-columns:1.4fr 1fr 1fr 1.2fr; gap:44px; padding-bottom:54px;}
  .footer-col h4{
    font-size:var(--text-base); font-weight:500; color:var(--ink); margin-bottom:18px;
  }
  .footer-brand-col .brand{margin-bottom:16px;}
  .footer-about{font-size:var(--text-sm); font-weight:400; line-height:1.9; color:var(--ink-dim); max-width:34ch; margin-bottom:20px;}
  .footer-links{display:flex; flex-direction:column; gap:10px;}
  .footer-links a{color:var(--ink-dim); font-weight:400; transition:color .2s; width:fit-content;}
  .footer-links a:hover{color:var(--accent);}
  .footer-contact{display:flex; flex-direction:column; gap:12px;}
  .footer-contact-row{display:flex; align-items:flex-start; gap:10px; color:var(--ink-dim); font-weight:400; line-height:1.7;}
  .footer-contact-row svg{width:16px; height:16px; flex-shrink:0; margin-top:5px; color:var(--accent);}
  .footer-contact-row a{transition:color .2s;}
  .footer-contact-row a:hover{color:var(--accent);}
  .footer-contact-row .ltr{direction:ltr; unicode-bidi:embed;}
  .footer-bottom{
    display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px;
    padding-block:22px; border-top:1px solid var(--line);
    font-size:var(--text-sm); font-weight:400; color:var(--ink-faint);
  }
  .footer-bottom a:hover{color:var(--accent);}
  @media(max-width:1000px){ .footer-grid{grid-template-columns:1fr 1fr; gap:36px;} }
  @media(max-width:560px){ .footer-grid{grid-template-columns:1fr; gap:32px;} footer.site-footer{padding-block:52px 0;} }

  /* ==========================================================================
     ADMIN CONSOLE
     --------------------------------------------------------------------------
     The panel used to inherit the public site's marketing typography (16-18px
     body, 46px inputs, 30px card padding). That scale is right for a landing
     page read at arm's length and wrong for a dense tool, where it turned every
     tab into a long scroll. Everything below is admin-local and deliberately
     tighter: fixed px sizes instead of the site's viewport clamps, so rows stay
     on a predictable grid at any window width.

     All of it is scoped under #adminRoot — the public site keeps its own type
     scale, buttons and fields untouched.
     ========================================================================== */
  #adminRoot{
    display:none; min-height:100vh; background:var(--adm-bg);

    /* ---- surfaces: four steps of depth instead of one flat border ---- */
    --adm-bg:#0b0b0c;
    --adm-surface:#111113;
    --adm-surface-2:#161619;
    --adm-surface-3:#1c1c20;
    --adm-line:#26262b;
    --adm-line-soft:#1b1b1f;
    --adm-line-strong:#34343b;

    /* ---- ink ---- */
    --adm-ink:#ece9e4;
    --adm-ink-dim:#9d9a95;
    --adm-ink-faint:#6b6863;

    /* ---- accents: gold stays the brand; the rest carry state ---- */
    --adm-gold:#c9a468;
    --adm-gold-soft:rgba(201,164,104,.13);
    --adm-gold-line:rgba(201,164,104,.34);
    --adm-ok:#57ab7f;
    --adm-warn:#d3a04a;
    --adm-danger:#e05a5f;
    --adm-info:#6f9fd8;

    /* ---- admin type scale (fixed px — see note above) ---- */
    --adm-t-micro:10.5px;
    --adm-t-xs:11.5px;
    --adm-t-sm:12.5px;
    --adm-t-md:13.5px;
    --adm-t-lg:15px;
    --adm-t-xl:18px;
    --adm-t-2xl:23px;
    --adm-t-3xl:30px;

    --adm-r-sm:8px;
    --adm-r-md:12px;
    --adm-r-lg:16px;
    --adm-shadow:0 18px 44px -26px rgba(0,0,0,.9);
    --adm-shadow-lift:0 26px 60px -18px rgba(0,0,0,.85);
    /* a 1px top highlight is what separates a raised panel from a drawn box */
    --adm-bevel:inset 0 1px 0 rgba(255,255,255,.035);

    color:var(--adm-ink);
    font-size:var(--adm-t-md);
  }
  #adminRoot.open{display:block;}
  .admin-shell-v2{display:flex; min-height:100vh;}
  #adminRoot .mono{font-family:var(--font-mono);}

  /* ---------- SIDEBAR ---------- */
  .admin-sidebar{
    width:252px; flex-shrink:0; border-inline-end:1px solid var(--adm-line-soft);
    background:linear-gradient(180deg,#111113 0%,#0d0d0f 100%);
    display:flex; flex-direction:column; height:100vh; position:sticky; top:0; overflow:hidden;
    transition:width var(--transition-normal);
  }
  .admin-sidebar.collapsed{width:72px;}
  .admin-sidebar.collapsed .label,
  .admin-sidebar.collapsed .admin-sidebar-brand .label,
  .admin-sidebar.collapsed .admin-nav-group-label{display:none;}
  .admin-sidebar.collapsed .admin-nav-item{justify-content:center; padding-inline:0;}

  .admin-sidebar-top{
    display:flex; align-items:center; justify-content:space-between; gap:10px;
    padding:18px 16px; border-bottom:1px solid var(--adm-line-soft); flex-shrink:0;
  }
  .admin-sidebar-brand{
    display:flex; align-items:center; gap:10px; font-size:var(--adm-t-md); font-weight:500;
    white-space:nowrap; overflow:hidden; letter-spacing:.01em;
  }
  .admin-sidebar-brand .dot{
    width:26px; height:26px; border-radius:8px; flex-shrink:0; position:relative;
    background:linear-gradient(140deg,var(--adm-gold),#8f7038);
    box-shadow:0 0 0 1px rgba(201,164,104,.25), 0 6px 16px -8px var(--adm-gold);
  }
  /* aperture blades: a camera iris reads instantly as "this is the film panel" */
  .admin-sidebar-brand .dot::after{
    content:''; position:absolute; inset:7px; border-radius:50%;
    border:1.5px solid rgba(12,10,6,.75);
  }
  .admin-sidebar-collapse{
    width:26px; height:26px; border-radius:7px; border:1px solid var(--adm-line); background:transparent;
    color:var(--adm-ink-faint); display:flex; align-items:center; justify-content:center; flex-shrink:0;
    font-size:var(--adm-t-sm); transition:all var(--transition-fast);
  }
  .admin-sidebar-collapse:hover{color:var(--adm-gold); border-color:var(--adm-gold-line);}
  .admin-sidebar.collapsed .admin-sidebar-collapse{transform:rotate(180deg);}

  .admin-sidebar-nav{padding:10px 10px 20px; flex:1; overflow-y:auto; overflow-x:hidden;}
  .admin-sidebar-nav::-webkit-scrollbar{width:6px;}
  .admin-sidebar-nav::-webkit-scrollbar-thumb{background:var(--adm-line); border-radius:3px;}
  .admin-nav-group-label{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.16em; text-transform:uppercase;
    color:var(--adm-ink-faint); padding:18px 12px 7px; white-space:nowrap;
  }
  .admin-nav-group:first-child .admin-nav-group-label{padding-top:8px;}
  .admin-nav-item{
    position:relative; display:flex; align-items:center; gap:11px; width:100%;
    padding:9px 12px; border-radius:9px; margin-bottom:1px;
    color:var(--adm-ink-dim); font-size:var(--adm-t-md); white-space:nowrap; overflow:hidden;
    background:none; border:none; text-align:start; line-height:1.35;
    transition:background var(--transition-fast), color var(--transition-fast);
  }
  .admin-nav-item svg{width:17px; height:17px; flex-shrink:0; opacity:.85;}
  .admin-nav-item:hover{background:var(--adm-surface-2); color:var(--adm-ink);}
  .admin-nav-item.active{
    background:linear-gradient(to inline-end, var(--adm-gold-soft), rgba(201,164,104,.02));
    color:var(--adm-gold);
  }
  .admin-nav-item.active svg{opacity:1;}
  /* the rail, not the fill, is what the eye locks onto when scanning the list */
  .admin-nav-item.active::before{
    content:''; position:absolute; inset-block:7px; inset-inline-start:0;
    width:2.5px; border-radius:0 3px 3px 0; background:var(--adm-gold);
  }
  .admin-nav-item .badge{
    margin-inline-start:auto; background:var(--adm-gold); color:#17120a;
    font-family:var(--font-mono); font-size:var(--adm-t-micro); font-weight:700;
    min-width:18px; height:18px; border-radius:9px; display:flex; align-items:center;
    justify-content:center; padding:0 5px; flex-shrink:0;
  }
  .admin-sidebar.collapsed .admin-nav-item .badge{
    position:absolute; top:4px; inset-inline-end:6px; min-width:7px; height:7px;
    padding:0; border-radius:50%; font-size:0;
  }
  .admin-sidebar-bottom{padding:10px; border-top:1px solid var(--adm-line-soft); flex-shrink:0;}

  /* ---------- TOPBAR ---------- */
  .admin-main{flex:1; min-width:0; display:flex; flex-direction:column;}
  .admin-topbar{
    position:sticky; top:0; z-index:30; display:flex; justify-content:space-between; align-items:center; gap:16px;
    padding:13px 26px; border-bottom:1px solid var(--adm-line-soft);
    background:rgba(11,11,12,.86); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  }
  .admin-topbar-left{display:flex; align-items:center; gap:13px; min-width:0;}
  .admin-mobile-menu-btn{
    display:none; width:34px; height:34px; align-items:center; justify-content:center;
    color:var(--adm-ink-dim); border-radius:8px; border:1px solid var(--adm-line); background:transparent;
  }
  .admin-mobile-menu-btn svg{width:17px; height:17px;}
  .admin-breadcrumb{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); color:var(--adm-ink-faint);
    letter-spacing:.13em; text-transform:uppercase; margin-bottom:3px; white-space:nowrap;
    overflow:hidden; text-overflow:ellipsis;
  }
  .admin-page-title{font-size:var(--adm-t-xl); font-weight:500; line-height:1.25; letter-spacing:-.005em;}
  .admin-topbar-right{display:flex; align-items:center; gap:10px; flex-shrink:0;}

  /* save state: one pill that swaps between idle / dirty / saved, so "did that
     take?" is answerable without scrolling back to the button you pressed */
  .admin-save-status{
    display:flex; align-items:center; gap:7px; font-family:var(--font-mono);
    font-size:var(--adm-t-xs); color:var(--adm-ink-faint); letter-spacing:.04em;
    padding:6px 12px; border-radius:999px; border:1px solid transparent; white-space:nowrap;
    transition:color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
  }
  .admin-save-status::before{
    content:''; width:6px; height:6px; border-radius:50%; background:currentColor; flex-shrink:0;
  }
  .admin-save-status.saved{color:var(--adm-ok); border-color:rgba(87,171,127,.3); background:rgba(87,171,127,.09);}
  .admin-save-status.dirty{color:var(--adm-warn); border-color:rgba(211,160,74,.32); background:rgba(211,160,74,.09);}
  .admin-save-status.dirty::before{animation:admPulse 1.6s ease-in-out infinite;}
  @keyframes admPulse{0%,100%{opacity:1;} 50%{opacity:.25;}}

  .admin-content-scroll{flex:1; padding:24px 26px 96px; max-width:1500px; width:100%;}
  .admin-sidebar-backdrop{display:none; position:fixed; inset:0; background:rgba(0,0,0,.66); z-index:199;}
  .admin-sidebar-backdrop.open{display:block;}

  /* ---------- PANELS ----------
     Every tab is a stack of <details> panels. Native disclosure gives keyboard
     and screen-reader behaviour for free, and — the reason it matters here —
     a closed panel keeps its inputs in the DOM, so the existing save handlers
     that read them by id keep working whether or not the panel is open. */
  .apanel{
    background:linear-gradient(180deg,var(--adm-surface) 0%,#0f0f11 100%);
    border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-lg);
    margin-bottom:14px; box-shadow:var(--adm-bevel); overflow:hidden;
    transition:border-color var(--transition-fast);
  }
  .apanel[open]{border-color:var(--adm-line);}
  .apanel:hover{border-color:var(--adm-line);}
  .apanel > summary{
    display:flex; align-items:center; gap:13px; padding:15px 18px; cursor:pointer;
    list-style:none; user-select:none;
  }
  .apanel > summary::-webkit-details-marker{display:none;}
  .apanel-icon{
    width:32px; height:32px; border-radius:9px; flex-shrink:0; display:flex; align-items:center; justify-content:center;
    background:var(--adm-surface-3); border:1px solid var(--adm-line); color:var(--adm-ink-dim);
    transition:all var(--transition-fast);
  }
  .apanel-icon svg{width:16px; height:16px;}
  .apanel[open] .apanel-icon{background:var(--adm-gold-soft); border-color:var(--adm-gold-line); color:var(--adm-gold);}
  .apanel-head-text{min-width:0; flex:1;}
  .apanel-title{display:block; font-size:var(--adm-t-lg); font-weight:500; line-height:1.3; letter-spacing:-.005em;}
  .apanel-sub{
    font-size:var(--adm-t-xs); color:var(--adm-ink-faint); margin-top:3px; line-height:1.5;
    display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden;
  }
  /* details>summary defaults to display:list-item, which ignores flex on the
     summary itself in Safari — the explicit flex above needs this to stick */
  .apanel > summary{display:flex;}
  .apanel-tag{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.1em; text-transform:uppercase;
    color:var(--adm-ink-faint); border:1px solid var(--adm-line); border-radius:999px; padding:3px 9px; flex-shrink:0;
  }
  .apanel-tag.adv{color:var(--adm-info); border-color:rgba(111,159,216,.28); background:rgba(111,159,216,.07);}
  .apanel-chevron{
    width:22px; height:22px; flex-shrink:0; color:var(--adm-ink-faint); display:flex; align-items:center; justify-content:center;
    transition:transform var(--transition-fast);
  }
  .apanel-chevron svg{width:13px; height:13px;}
  .apanel[open] .apanel-chevron{transform:rotate(180deg);}
  .apanel-body{padding:4px 18px 18px; border-top:1px solid var(--adm-line-soft); margin-top:2px; padding-top:18px;}
  .apanel-body > :first-child{margin-top:0;}

  /* a lighter grouping unit for use *inside* a panel body */
  .asub{margin-top:22px; padding-top:18px; border-top:1px dashed var(--adm-line);}
  .asub:first-child{margin-top:0; padding-top:0; border-top:none;}
  .asub-title{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.15em; text-transform:uppercase;
    color:var(--adm-gold); margin-bottom:14px;
  }

  /* ---------- FORM CONTROLS (admin-scoped overrides) ---------- */
  #adminRoot .field{margin-bottom:0;}
  #adminRoot .field label{
    display:block; font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.11em;
    text-transform:uppercase; color:var(--adm-ink-faint); margin-bottom:7px; line-height:1.5; text-wrap:balance;
  }
  /* The service-page editors build their own rows rather than .field wrappers,
     so they are named here too — without this their inputs fall back to the
     browser default and render as white bars on the dark panel. */
  #adminRoot .field input,
  #adminRoot .field textarea,
  #adminRoot .field select,
  #adminRoot .svc-row input,
  #adminRoot .svc-card input:not([type=checkbox]),
  #adminRoot .svc-card textarea{
    width:100%; background:var(--adm-surface-2); border:1px solid var(--adm-line); border-radius:var(--adm-r-sm);
    color:var(--adm-ink); padding:9px 12px; font-family:var(--font-fa); font-size:var(--adm-t-md); line-height:1.6;
    transition:border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
  }
  #adminRoot .svc-row input::placeholder,
  #adminRoot .svc-card input::placeholder,
  #adminRoot .svc-card textarea::placeholder{color:var(--adm-ink-faint); opacity:.65;}
  #adminRoot .svc-row input:focus,
  #adminRoot .svc-card input:focus,
  #adminRoot .svc-card textarea:focus{border-color:var(--adm-accent); outline:none;}
  #adminRoot .field input,
  #adminRoot .field select,
  #adminRoot .svc-row input,
  #adminRoot .svc-card input:not([type=checkbox]){min-height:38px;}
  #adminRoot .field select{cursor:pointer; appearance:none; padding-inline-end:30px;
    background-image:linear-gradient(45deg,transparent 50%,var(--adm-ink-faint) 50%),linear-gradient(135deg,var(--adm-ink-faint) 50%,transparent 50%);
    background-size:5px 5px,5px 5px; background-repeat:no-repeat;
    background-position:calc(100% - 16px) 17px,calc(100% - 11px) 17px;
  }
  #adminRoot .field textarea{min-height:76px; resize:vertical;}
  #adminRoot .field input::placeholder,
  #adminRoot .field textarea::placeholder{color:var(--adm-ink-faint); opacity:.65;}
  #adminRoot .field input:hover,
  #adminRoot .field textarea:hover,
  #adminRoot .field select:hover{border-color:var(--adm-line-strong);}
  #adminRoot .field input:focus,
  #adminRoot .field textarea:focus,
  #adminRoot .field select:focus{
    outline:none; border-color:var(--adm-gold-line); background:var(--adm-surface-3);
    box-shadow:0 0 0 3px rgba(201,164,104,.11);
  }
  #adminRoot .field input[type=file]{
    padding:7px 10px; font-family:var(--font-mono); font-size:var(--adm-t-xs); color:var(--adm-ink-faint);
    background:transparent; border-style:dashed; cursor:pointer;
  }
  #adminRoot .field input[type=file]:hover{border-color:var(--adm-gold-line); color:var(--adm-ink-dim);}
  #adminRoot .field input[type=file]::file-selector-button{
    background:var(--adm-surface-3); border:1px solid var(--adm-line); border-radius:6px; color:var(--adm-ink-dim);
    padding:5px 11px; margin-inline-end:11px; font-family:var(--font-fa); font-size:var(--adm-t-xs); cursor:pointer;
  }
  #adminRoot .field input[type=range]{
    padding:0; min-height:auto; border:none; background:transparent; accent-color:var(--adm-gold); cursor:pointer;
  }
  #adminRoot .field input[type=range]:focus{box-shadow:none;}

  /* checkbox rendered as a switch row — a bare 18px box in a 1fr grid cell read
     as an unlabelled stray, and these are the controls that turn whole sections
     of the public site on and off */
  .aswitch{
    display:flex; align-items:center; gap:11px; cursor:pointer; padding:10px 13px;
    border:1px solid var(--adm-line); border-radius:var(--adm-r-sm); background:var(--adm-surface-2);
    font-size:var(--adm-t-md); color:var(--adm-ink-dim); line-height:1.6; transition:all var(--transition-fast);
  }
  .aswitch:hover{border-color:var(--adm-line-strong); color:var(--adm-ink);}
  .aswitch input[type=checkbox]{
    appearance:none; width:34px; height:20px; border-radius:999px; flex-shrink:0; cursor:pointer;
    background:var(--adm-surface-3); border:1px solid var(--adm-line-strong); position:relative;
    transition:background var(--transition-fast), border-color var(--transition-fast);
  }
  .aswitch input[type=checkbox]::after{
    content:''; position:absolute; top:2px; inset-inline-start:2px; width:14px; height:14px; border-radius:50%;
    background:var(--adm-ink-faint); transition:transform var(--transition-fast), background var(--transition-fast);
  }
  .aswitch input[type=checkbox]:checked{background:var(--adm-gold-soft); border-color:var(--adm-gold-line);}
  .aswitch input[type=checkbox]:checked::after{background:var(--adm-gold); transform:translateX(14px);}
  html[dir=rtl] .aswitch input[type=checkbox]:checked::after{transform:translateX(-14px);}
  .aswitch:has(input:checked){color:var(--adm-ink); border-color:var(--adm-gold-line);}

  /* Which service landing pages a video is filed under. Checkboxes generated
     from the existing service pages rather than a free-text tag box, so a
     typo can't quietly detach a video from the page it belongs on. */
  .svc-tag-picker{display:flex; flex-wrap:wrap; gap:8px;}
  .svc-tag{display:inline-flex; align-items:center; gap:8px; padding:8px 14px; border:1px solid var(--adm-line);
           border-radius:999px; cursor:pointer; font-size:.86rem; color:var(--adm-ink-dim); transition:border-color .2s, color .2s;}
  .svc-tag:hover{border-color:var(--adm-line-strong); color:var(--adm-ink);}
  .svc-tag input{accent-color:var(--adm-gold); margin:0;}
  .svc-tag:has(input:checked){color:var(--adm-ink); border-color:var(--adm-gold-line); background:var(--adm-gold-soft);}

  /* Icon picker for a service page. Radios rather than a <select> so the admin
     picks the shape by looking at it instead of reading its key name. */
  .svc-icon-picker{display:flex; flex-wrap:wrap; gap:8px;}
  .svc-icon-opt{display:inline-flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;
                width:88px; padding:13px 8px; border:1px solid var(--adm-line); border-radius:12px;
                color:var(--adm-ink-faint); font-size:var(--adm-t-xs); text-align:center; line-height:1.4;
                transition:border-color .2s, color .2s, background .2s;}
  .svc-icon-opt svg{width:22px; height:22px;}
  .svc-icon-opt input{position:absolute; opacity:0; width:0; height:0;}
  .svc-icon-opt:hover{border-color:var(--adm-line-strong); color:var(--adm-ink);}
  .svc-icon-opt:has(input:checked){border-color:var(--adm-gold-line); background:var(--adm-gold-soft); color:var(--adm-gold);}

  #adminRoot .form-grid{display:grid; grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); gap:16px 16px; max-width:none;}
  #adminRoot .form-grid .full{grid-column:1/-1;}
  /* a row that must stay narrow even in an auto-fit grid (px counters, years) */
  #adminRoot .form-grid.tight{grid-template-columns:repeat(auto-fit, minmax(150px,1fr));}

  /* ---------- BUTTONS (admin-scoped) ---------- */
  #adminRoot .btn{
    display:inline-flex; align-items:center; justify-content:center; gap:8px; border-radius:var(--adm-r-sm);
    padding:0 16px; min-height:36px; font-family:var(--font-fa); font-size:var(--adm-t-md); font-weight:400;
    letter-spacing:0; border:1px solid var(--adm-line); background:var(--adm-surface-2); color:var(--adm-ink-dim);
    transition:all var(--transition-fast); white-space:nowrap;
  }
  #adminRoot .btn:hover{background:var(--adm-surface-3); color:var(--adm-ink); border-color:var(--adm-line-strong);}
  #adminRoot .btn.primary{
    background:linear-gradient(180deg,#d3ae72,#bd9659); border-color:#d8b478; color:#1a1409; font-weight:600;
    box-shadow:0 6px 18px -10px rgba(201,164,104,.75);
  }
  #adminRoot .btn.primary:hover{background:linear-gradient(180deg,#dfbc82,#c9a468); color:#1a1409;}
  #adminRoot .btn.ghost{background:transparent; color:var(--adm-ink-dim); border-color:var(--adm-line);}
  #adminRoot .btn.ghost:hover{background:var(--adm-surface-2); color:var(--adm-ink); border-color:var(--adm-line-strong);}
  #adminRoot .btn.danger{background:transparent; border-color:rgba(224,90,95,.35); color:var(--adm-danger);}
  #adminRoot .btn.danger:hover{background:var(--adm-danger); border-color:var(--adm-danger); color:#fff;}
  #adminRoot .btn.sm{min-height:30px; padding:0 11px; font-size:var(--adm-t-sm);}
  #adminRoot .btn:disabled{opacity:.45; pointer-events:none;}
  #adminRoot .btn svg{width:15px; height:15px; flex-shrink:0;}

  /* form footer: the save button sits on a divider so it never floats free in
     the middle of a long field grid */
  .aform-actions{
    display:flex; align-items:center; gap:10px; flex-wrap:wrap;
    margin-top:20px; padding-top:16px; border-top:1px solid var(--adm-line-soft);
  }
  .aform-actions .spacer{margin-inline-start:auto;}

  /* ---------- service page visual editors ---------- */
  .svc-editor{display:flex; flex-direction:column; gap:10px; margin-top:4px;}
  .svc-row{display:flex; align-items:center; gap:8px;}
  .svc-row input{flex:1; min-width:0;}
  .svc-row.sh .f-years{flex:0 0 150px;}
  .svc-row.pay .f-pct{flex:0 0 80px; text-align:center;}
  .svc-row.stat .f-svalue{flex:0 0 90px; text-align:center;}
  .svc-row.opt .f-oprice{flex:0 0 140px;}
  .svc-del{
    flex:0 0 auto; width:34px; height:34px; display:inline-flex; align-items:center;
    justify-content:center; border:1px solid var(--adm-line-soft); border-radius:9px;
    color:var(--adm-ink-faint); background:transparent; transition:color .2s, border-color .2s;
  }
  .svc-del:hover{color:#e5484d; border-color:#e5484d;}
  .svc-del svg{width:15px; height:15px;}
  .svc-add{
    margin-top:12px; padding:9px 16px; border:1px dashed var(--adm-line);
    border-radius:9px; color:var(--adm-ink-dim); font-size:.86rem; background:transparent;
    transition:color .2s, border-color .2s;
  }
  .svc-add:hover{color:var(--adm-accent); border-color:var(--adm-accent);}
  .svc-add-row{display:flex; gap:8px; flex-wrap:wrap;}

  .svc-card{
    border:1px solid var(--adm-line-soft); border-radius:12px; padding:16px;
    background:var(--adm-panel-2, rgba(255,255,255,.02)); display:flex; flex-direction:column; gap:12px;
  }
  .svc-card-head{display:flex; align-items:center; gap:8px;}
  .svc-card-head .f-name, .svc-card-head .f-label{flex:1; min-width:0; font-weight:500;}
  .svc-kind{
    flex:0 0 auto; font-size:.74rem; color:var(--adm-accent); padding:4px 10px;
    border:1px solid var(--adm-line-soft); border-radius:999px; white-space:nowrap;
  }
  .svc-card-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:10px;}
  .svc-card label, .svc-card .full{display:flex; flex-direction:column; gap:6px; font-size:.8rem; color:var(--adm-ink-faint);}
  .svc-card label.aswitch{flex-direction:row; align-items:center; gap:9px;}
  .svc-sub{border-top:1px solid var(--adm-line-soft); padding-top:12px;}
  .svc-sub-h{display:block; font-size:.78rem; color:var(--adm-ink-faint); margin-bottom:9px;}
  .svc-sub .svc-add{margin-top:9px;}
  .f-feats, .f-opts{display:flex; flex-direction:column; gap:8px;}
  /* Grouped digits are easier to check than a run of zeros, and the value is
     Latin-only so the mono face is safe here. */
  input.money{font-family:var(--font-mono, monospace); letter-spacing:.04em;}

  #adminRoot .icon-btn{
    display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:30px; padding:0 10px;
    border-radius:7px; border:1px solid var(--adm-line); background:var(--adm-surface-2);
    color:var(--adm-ink-dim); font-size:var(--adm-t-sm); font-family:var(--font-fa); transition:all var(--transition-fast);
  }
  #adminRoot .icon-btn:hover{color:var(--adm-ink); border-color:var(--adm-line-strong); background:var(--adm-surface-3);}
  #adminRoot .icon-btn.danger:hover{color:var(--adm-danger); border-color:rgba(224,90,95,.4);}
  #adminRoot .icon-btn svg{width:14px; height:14px;}

  /* ---------- NOTES / HINTS ----------
     These used to be long mono paragraphs stacked inside every card. Same text,
     but marked as an aside so it reads as guidance rather than as content. */
  #adminRoot .small-note{
    font-family:var(--font-fa); font-size:var(--adm-t-sm); color:var(--adm-ink-faint);
    line-height:1.85; margin-top:12px; letter-spacing:0;
  }
  .ahint{
    display:flex; gap:10px; padding:11px 13px; border-radius:var(--adm-r-sm);
    background:var(--adm-surface-2); border:1px solid var(--adm-line-soft);
    font-size:var(--adm-t-sm); color:var(--adm-ink-dim); line-height:1.8; margin-bottom:16px;
  }
  .ahint::before{
    content:''; width:2px; border-radius:2px; background:var(--adm-line-strong); flex-shrink:0; align-self:stretch;
  }
  .ahint b{color:var(--adm-ink); font-weight:500;}
  .ahint.warn{background:rgba(211,160,74,.07); border-color:rgba(211,160,74,.22); color:#cfae78;}
  .ahint.warn::before{background:var(--adm-warn);}
  .ahint.warn b{color:var(--adm-warn);}
  .ahint.editing{background:var(--adm-gold-soft); border-color:var(--adm-gold-line); color:var(--adm-gold);}
  .ahint.editing::before{background:var(--adm-gold);}
  #adminRoot code{
    font-family:var(--font-mono); font-size:.92em; background:var(--adm-surface-3);
    border:1px solid var(--adm-line); border-radius:5px; padding:1px 6px; color:var(--adm-gold);
  }

  /* ---------- TOOLBAR (search + filters above a list) ---------- */
  .atoolbar{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:16px;}
  .asearch{position:relative; flex:1; min-width:190px;}
  .asearch svg{
    position:absolute; inset-inline-start:11px; top:50%; transform:translateY(-50%);
    width:14px; height:14px; color:var(--adm-ink-faint); pointer-events:none;
  }
  .asearch input{
    width:100%; min-height:36px; padding:0 12px; padding-inline-start:34px;
    background:var(--adm-surface-2); border:1px solid var(--adm-line); border-radius:var(--adm-r-sm);
    color:var(--adm-ink); font-family:var(--font-fa); font-size:var(--adm-t-md);
    transition:border-color var(--transition-fast), box-shadow var(--transition-fast);
  }
  .asearch input::placeholder{color:var(--adm-ink-faint);}
  .asearch input:focus{outline:none; border-color:var(--adm-gold-line); box-shadow:0 0 0 3px rgba(201,164,104,.11);}
  .aseg{display:inline-flex; padding:3px; gap:2px; background:var(--adm-surface-2); border:1px solid var(--adm-line); border-radius:10px;}
  .aseg button{
    padding:0 13px; min-height:28px; border-radius:7px; border:none; background:transparent;
    color:var(--adm-ink-faint); font-family:var(--font-fa); font-size:var(--adm-t-sm); white-space:nowrap;
    transition:all var(--transition-fast);
  }
  .aseg button:hover{color:var(--adm-ink);}
  .aseg button.on{background:var(--adm-surface-3); color:var(--adm-gold); box-shadow:var(--adm-bevel);}
  .aseg .n{font-family:var(--font-mono); font-size:var(--adm-t-micro); opacity:.65; margin-inline-start:5px;}
  .acount{font-family:var(--font-mono); font-size:var(--adm-t-xs); color:var(--adm-ink-faint); letter-spacing:.06em; white-space:nowrap;}

  /* ---------- LIST ROWS ---------- */
  .alist{display:flex; flex-direction:column; gap:7px;}
  .arow{
    display:grid; grid-template-columns:auto 62px 1fr auto; gap:13px; align-items:center;
    padding:9px 12px; border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-md);
    background:var(--adm-surface); transition:border-color var(--transition-fast), background var(--transition-fast);
  }
  .arow:hover{border-color:var(--adm-line); background:var(--adm-surface-2);}
  .arow-media{
    width:62px; height:44px; border-radius:7px; object-fit:cover; background:var(--adm-surface-3);
    border:1px solid var(--adm-line-soft); display:block;
  }
  .arow-media.contain{object-fit:contain; padding:5px;}
  .arow-text{min-width:0;}
  .arow-title{
    font-size:var(--adm-t-md); color:var(--adm-ink); line-height:1.4; margin-bottom:3px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .arow-meta{
    font-family:var(--font-mono); font-size:var(--adm-t-xs); color:var(--adm-ink-faint);
    letter-spacing:.03em; display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  }
  .arow-actions{display:flex; gap:6px; flex-shrink:0;}
  .adot{width:5px; height:5px; border-radius:50%; background:var(--adm-line-strong); flex-shrink:0;}
  .adot.on{background:var(--adm-ok); box-shadow:0 0 7px rgba(87,171,127,.55);}
  .adot.off{background:var(--adm-ink-faint);}

  /* ---------- DRAG AND DROP ----------
     See makeSortable(): pointer-events based, so these states apply equally to
     mouse and touch. The old native-HTML5 version had no touch path at all. */
  .adrag{
    width:24px; height:34px; display:flex; align-items:center; justify-content:center; flex-shrink:0;
    color:var(--adm-ink-faint); cursor:grab; border-radius:6px; touch-action:none;
    transition:color var(--transition-fast), background var(--transition-fast);
  }
  .adrag:hover{color:var(--adm-gold); background:var(--adm-surface-3);}
  .adrag:active{cursor:grabbing;}
  .adrag svg{width:14px; height:14px; pointer-events:none;}
  .sortable-on .arow,
  .sortable-on .vcard{transition:transform 180ms cubic-bezier(.2,.8,.3,1), border-color var(--transition-fast);}
  .sort-dragging{
    opacity:.97; box-shadow:var(--adm-shadow-lift); border-color:var(--adm-gold-line) !important;
    position:relative; z-index:40; cursor:grabbing;
  }
  .arow.sort-dragging{transform:scale(1.012);}
  .vcard.sort-dragging{transform:scale(1.04) rotate(-1.1deg);}
  /* the gap the dragged item will drop into */
  .sort-gap{
    border:1px dashed var(--adm-gold-line) !important; background:var(--adm-gold-soft) !important;
    border-radius:var(--adm-r-md);
  }
  .sort-gap > *{opacity:0;}
  body.sorting-active{cursor:grabbing; user-select:none; -webkit-user-select:none;}

  /* ---------- VIDEO GRID ----------
     55 videos in a 64x48-thumbnail list was unscannable. Cards are big enough
     to recognise a frame, and the order index is on the card because the whole
     point of this view is arranging that order. */
  .vgrid{display:grid; grid-template-columns:repeat(auto-fill, minmax(204px,1fr)); gap:13px;}
  .vcard{
    position:relative; border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-md); overflow:hidden;
    background:var(--adm-surface); box-shadow:var(--adm-bevel); transition:border-color var(--transition-fast), transform var(--transition-fast);
  }
  .vcard:hover{border-color:var(--adm-line-strong);}
  .vcard.editing{border-color:var(--adm-gold); box-shadow:0 0 0 1px var(--adm-gold-line);}
  .vcard-cover{position:relative; aspect-ratio:16/10; background:#000; overflow:hidden; display:block;}
  .vcard-cover img{width:100%; height:100%; object-fit:cover; display:block;}
  .vcard-cover::after{
    content:''; position:absolute; inset:0; background:linear-gradient(180deg,rgba(0,0,0,.5) 0%,transparent 34%,transparent 62%,rgba(0,0,0,.6) 100%);
    pointer-events:none;
  }
  .vcard-index{
    position:absolute; top:8px; inset-inline-start:8px; z-index:2;
    font-family:var(--font-mono); font-size:var(--adm-t-xs); font-weight:600; color:var(--adm-ink);
    background:rgba(8,8,9,.72); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,.13);
    border-radius:7px; min-width:24px; height:22px; display:flex; align-items:center; justify-content:center; padding:0 6px;
  }
  .vcard-grip{
    position:absolute; top:8px; inset-inline-end:8px; z-index:3;
    width:26px; height:22px; border-radius:7px; display:flex; align-items:center; justify-content:center;
    background:rgba(8,8,9,.72); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,.13);
    color:var(--adm-ink-dim); cursor:grab; touch-action:none;
    opacity:0; transition:opacity var(--transition-fast), color var(--transition-fast);
  }
  .vcard:hover .vcard-grip,
  .vcard-grip:focus-visible{opacity:1;}
  .vcard-grip:hover{color:var(--adm-gold);}
  .vcard-grip svg{width:13px; height:13px; pointer-events:none;}
  .vcard-badges{position:absolute; bottom:7px; inset-inline-start:8px; z-index:2; display:flex; gap:5px;}
  .vchip{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.06em;
    background:rgba(8,8,9,.72); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,.13);
    border-radius:5px; padding:2px 6px; color:var(--adm-ink-dim);
  }
  .vchip.portrait{color:#c6a9e0; border-color:rgba(198,169,224,.28);}
  .vchip.nolink{color:var(--adm-warn); border-color:rgba(211,160,74,.3);}
  .vcard-body{padding:10px 11px 11px;}
  .vcard-title{
    font-size:var(--adm-t-md); line-height:1.45; color:var(--adm-ink); margin-bottom:7px;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; min-height:2.9em;
  }
  .vcard-actions{display:flex; gap:5px;}
  .vcard-actions .icon-btn{flex:1; padding:0 7px; min-height:28px; font-size:var(--adm-t-xs);}
  /* Touch devices never fire :hover, so a hover-only grip is a grip that can
     never be grabbed. Two independent signals rather than one: some hybrid
     and tablet browsers report `hover:hover` despite having no mouse, and an
     invisible drag handle is a much worse failure than an always-visible one. */
  @media(hover:none), (pointer:coarse){ .vcard-grip{opacity:1;} }

  /* ---------- DASHBOARD ---------- */
  .dash-grid{display:grid; grid-template-columns:repeat(auto-fit, minmax(148px,1fr)); gap:11px; margin-bottom:14px;}
  .dash-card{
    position:relative; border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-md); padding:15px 16px;
    background:linear-gradient(180deg,var(--adm-surface) 0%,#0f0f11 100%); box-shadow:var(--adm-bevel);
    text-align:start; transition:border-color var(--transition-fast), transform var(--transition-fast); overflow:hidden;
  }
  button.dash-card:hover{border-color:var(--adm-gold-line); transform:translateY(-2px);}
  .dash-card .ico{
    width:26px; height:26px; border-radius:7px; display:flex; align-items:center; justify-content:center;
    background:var(--adm-surface-3); border:1px solid var(--adm-line); color:var(--adm-ink-faint); margin-bottom:11px;
  }
  .dash-card .ico svg{width:14px; height:14px;}
  .dash-card .n{font-family:var(--font-mono); font-size:var(--adm-t-3xl); font-weight:600; color:var(--adm-ink); line-height:1;}
  .dash-card .l{font-size:var(--adm-t-sm); color:var(--adm-ink-faint); margin-top:6px; line-height:1.5;}
  .dash-card.hot .ico{background:var(--adm-gold-soft); border-color:var(--adm-gold-line); color:var(--adm-gold);}
  .dash-card.hot .n{color:var(--adm-gold);}
  .dash-two-col{display:grid; grid-template-columns:1fr 1fr; gap:13px;}
  .dash-list-card{
    border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-lg); padding:16px 18px;
    background:linear-gradient(180deg,var(--adm-surface) 0%,#0f0f11 100%); box-shadow:var(--adm-bevel);
  }
  .dash-list-card h4{
    font-family:var(--font-mono); font-size:var(--adm-t-micro); letter-spacing:.15em; text-transform:uppercase;
    color:var(--adm-ink-faint); font-weight:400; margin-bottom:12px; display:flex; align-items:center; gap:8px;
  }
  .dash-list-card h4 svg{width:13px; height:13px; flex-shrink:0;}
  /* Icons are injected as bare <svg> strings with no intrinsic size, so a
     container that forgets to size them renders a 300px glyph. Deliberately
     not a blanket `#adminRoot svg{}` rule — the world-card and logo previews
     render the *site's* SVGs in here and must keep their real dimensions. */
  #adminRoot .aform-actions svg,
  #adminRoot .atoolbar svg,
  #adminRoot .arow-actions svg{width:14px; height:14px; flex-shrink:0;}
  .dash-list-row{
    display:flex; justify-content:space-between; align-items:center; gap:12px; padding:9px 0;
    border-top:1px solid var(--adm-line-soft); font-size:var(--adm-t-md); color:var(--adm-ink-dim);
  }
  .dash-list-row span:first-child{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .dash-list-row .mono{font-family:var(--font-mono); font-size:var(--adm-t-xs); color:var(--adm-ink-faint); flex-shrink:0;}
  .dash-list-card .dash-list-row:first-of-type{border-top:none;}
  .dash-quick{display:flex; gap:9px; flex-wrap:wrap; margin-bottom:14px;}
  @media(max-width:860px){ .dash-two-col{grid-template-columns:1fr;} }

  #adminRoot .empty-state{
    padding:40px 20px; text-align:center; border:1px dashed var(--adm-line); border-radius:var(--adm-r-md);
    color:var(--adm-ink-faint); font-family:var(--font-fa); font-size:var(--adm-t-md); line-height:1.8;
    background:var(--adm-surface);
  }
  #adminRoot .admin-grid-2{display:grid; grid-template-columns:1fr; gap:0; align-items:start;}
  #adminRoot .admin-panel-content h3{font-size:var(--adm-t-lg); line-height:1.3; margin-bottom:14px; font-weight:500;}
  .login-box{max-width:340px; margin:110px auto; text-align:center;}

  /* ---------- CONFIRM MODAL ---------- */
  .confirm-modal{position:fixed; inset:0; z-index:3000; display:none; align-items:center; justify-content:center; background:var(--overlay-bg-soft); padding:20px;}
  .confirm-modal.open{display:flex;}
  .confirm-modal-box{
    background:var(--adm-surface-2, #161619); border:1px solid var(--adm-line, #26262b);
    border-radius:16px; padding:22px; max-width:360px; width:100%; box-shadow:0 30px 70px -20px rgba(0,0,0,.9);
  }
  .confirm-modal-box p{font-size:13.5px; margin-bottom:18px; line-height:1.8; color:var(--adm-ink, #ece9e4);}
  .confirm-modal-actions{display:flex; gap:9px; justify-content:flex-end;}
  .btn.danger{border-color:#b6562c; color:#e2896a;}
  .btn.danger:hover{background:#b6562c; color:#fff;}

  @media(max-width:900px){
    .admin-sidebar{
      position:fixed; top:0; bottom:0; width:270px; z-index:200;
      inset-inline-start:-100%; transition:inset-inline-start var(--transition-normal);
    }
    .admin-sidebar.open{inset-inline-start:0;}
    .admin-sidebar.collapsed{width:270px;}
    .admin-sidebar.collapsed .label,
    .admin-sidebar.collapsed .admin-nav-group-label{display:block;}
    .admin-sidebar.collapsed .admin-nav-item{justify-content:flex-start; padding-inline:12px;}
    .admin-sidebar-collapse{display:none;}
    .admin-mobile-menu-btn{display:flex;}
    .admin-content-scroll{padding:16px 14px 72px;}
    .admin-topbar{padding:11px 14px;}
    .apanel > summary{padding:13px 14px;}
    .apanel-body{padding:16px 14px 16px;}
    .dash-grid{grid-template-columns:repeat(2, 1fr);}
    .vgrid{grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:10px;}
    /* third belt-and-braces path to a visible grip at phone/tablet widths */
    .vcard-grip{opacity:1;}
    .arow{grid-template-columns:auto 48px 1fr; gap:10px; padding:9px 10px;}
    .arow-media{width:48px; height:36px;}
    /* actions wrap under the text rather than squeezing it to nothing */
    .arow-actions{grid-column:2/-1; justify-content:flex-start;}
  }

  .login-box .brand{margin-bottom:32px; font-size:1rem; letter-spacing:.2em;}
  .form-grid{display:grid; grid-template-columns:1fr 1fr; gap:24px 20px; max-width:900px;}
  .form-grid .full{grid-column:1/-1;}
  .cover-thumbs{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
  .cover-thumbs img{width:110px; height:62px; object-fit:cover; border-radius:6px; cursor:pointer; border:2px solid transparent; transition:border-color .2s;}
  .cover-thumbs img:hover{border-color:var(--accent);}
  .cover-slider{width:100%; margin-top:14px; accent-color:var(--accent);}

  /* ---------- COVER PICKER MODAL ---------- */
  .cover-modal{position:fixed; inset:0; z-index:3000; display:none; align-items:center; justify-content:center; background:var(--overlay-bg-soft); padding:20px;}
  .cover-modal.open{display:flex;}
  .cover-modal-box{
    background:var(--bg-elev); border:1px solid var(--line); border-radius:var(--radius-md);
    width:100%; max-width:680px; max-height:88vh; overflow-y:auto; padding:24px;
  }
  .cover-modal-head{display:flex; justify-content:space-between; align-items:center; margin-bottom:18px;}
  .cover-modal-head h4{font-size:var(--text-base); font-weight:500;}
  .cover-modal-close{color:var(--ink-dim); font-size:1.2rem; padding:6px 10px;}
  .cover-modal-close:hover{color:var(--accent);}
  .cover-modal-box video{width:100%; max-height:44vh; border-radius:8px; background:#000; display:block;}
  /* the canvas is the live "this is exactly what gets saved" preview for the
     frame grabber, so the crop/zoom is visible before committing */
  .cover-modal-box canvas{display:block; width:100%; max-height:34vh; object-fit:contain; border-radius:8px; background:#000;}
  .cover-zoom-row{margin-top:16px; padding-top:16px; border-top:1px solid var(--line);}
  .cover-zoom-row label{display:block; font-size:var(--text-sm); color:var(--ink-dim); margin-bottom:2px;}
  .cover-zoom-row label b{color:var(--accent); font-family:'IBM Plex Mono';}
  .cover-zoom-presets{display:flex; gap:8px; flex-wrap:wrap; margin-top:10px;}
  .cover-zoom-presets .btn{padding:8px 14px; font-size:var(--text-xs); min-height:auto;}
  .cover-modal-box .small-note{margin-top:0; margin-bottom:12px;}
  .toast{position:fixed; bottom:30px; left:50%; transform:translateX(-50%); background:var(--ink); color:var(--bg); padding:12px 24px; border-radius:2px; font-family:'IBM Plex Mono'; font-size:var(--text-2xs); letter-spacing:.06em; z-index:3000; opacity:0; pointer-events:none; transition:opacity .3s, transform .3s;}
  .toast.show{opacity:1; transform:translateX(-50%) translateY(-6px);}
  .msg-card{
    border:1px solid var(--adm-line-soft); border-radius:var(--adm-r-md); padding:14px 16px; margin-bottom:9px;
    background:var(--adm-surface); font-size:var(--adm-t-md); line-height:1.8; color:var(--adm-ink-dim);
    transition:border-color var(--transition-fast);
  }
  .msg-card:hover{border-color:var(--adm-line);}
  .msg-card.unread{border-inline-start:2px solid var(--adm-gold); background:linear-gradient(to inline-end,var(--adm-gold-soft),var(--adm-surface) 40%);}
  .msg-card .top{
    display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;
    font-family:var(--font-mono); font-size:var(--adm-t-xs); color:var(--adm-ink-faint); margin-bottom:9px;
  }
  .msg-card .top b{font-family:var(--font-fa); font-size:var(--adm-t-md); color:var(--adm-ink); font-weight:500;}
  .msg-extra{
    font-size:var(--adm-t-xs); font-family:var(--font-mono); color:var(--adm-ink-faint);
    margin-top:10px; padding-top:9px; border-top:1px solid var(--adm-line-soft); line-height:1.9;
  }
  .small-note{font-family:'IBM Plex Mono'; font-size:var(--text-xs); color:var(--ink-faint); margin-top:14px; line-height:1.8;}

  /* ---- upload progress (injected under whichever file input is uploading) ----
     A video is tens or hundreds of MB, so the request can run for minutes with
     nothing on screen — which reads as a frozen panel rather than work in
     progress. Driven by XHR upload events; see uploadFileFull(). */
  .upload-progress{margin-top:10px;}
  .upload-progress-track{
    height:6px; border-radius:999px; background:var(--line); overflow:hidden;
  }
  .upload-progress-bar{
    height:100%; width:0; border-radius:999px; background:var(--accent);
    transition:width .15s linear;
  }
  /* server-side work after the bytes land (WebP sibling, logo trim) has no
     measurable progress, so the bar switches to an indeterminate sweep */
  .upload-progress.processing .upload-progress-bar{
    width:100%; background:linear-gradient(90deg, var(--line) 0%, var(--accent) 50%, var(--line) 100%);
    background-size:200% 100%; animation:uploadSweep 1.1s linear infinite;
  }
  @keyframes uploadSweep{ from{background-position:200% 0;} to{background-position:-200% 0;} }
  .upload-progress-text{
    margin-top:6px; font-family:var(--font-mono); font-size:var(--text-2xs);
    color:var(--ink-dim); letter-spacing:.04em; display:flex; justify-content:space-between; gap:12px;
  }
  .upload-progress.failed .upload-progress-bar{background:#e5484d; animation:none;}
  .upload-progress.failed .upload-progress-text{color:#e5484d;}
  @media (prefers-reduced-motion:reduce){
    .upload-progress-bar{transition:none;}
    .upload-progress.processing .upload-progress-bar{animation:none;}
  }

  @media(max-width:1080px){
    nav.links{display:none;}
    .nav-right .nav-cta{display:none;}
    .burger{display:flex;}
    .about-grid{grid-template-columns:1fr; gap:44px;}
    /* single column: portrait + its stat strip centre together and the sticky
       behaviour is dropped (handled by the min-width:901px rule) */
    .about-portrait-col{max-width:340px; margin-inline:auto; width:100%;}
    .form-grid{grid-template-columns:1fr;}
    section{padding-block:var(--section-space-mobile); padding-inline:var(--container-pad);}
    .contact-card{padding:34px 22px; border-radius:22px;}
    .services-grid{grid-template-columns:1fr;}
    .video-row{grid-template-columns:repeat(auto-fill, minmax(220px,1fr));}
  }
  @media (prefers-reduced-motion:reduce){
    .stagger-item{transition:none; opacity:1; transform:none;}
    .card.stagger-item{animation:none; opacity:1; transform:none;}
  }
