/* =========================================================
   v2.css
   The "V2" layer, a photo-led editorial direction.

   Every rule in this file is scoped under html[data-ui="v2"]:
   without that attribute (set by web/js/appearance.js) the sheet is
   entirely inert and the client-approved V1 is served untouched.
   None of the four V1 stylesheets is modified.

   Two levels, in this order:
     1. remapping the tokens.css variables — tokens.css is a real
        design system, and almost all of components.css/layout.css
        consumes var(--c-*), var(--r-*), var(--sp-*): redefining those
        tokens reskins the whole application without duplicating a
        single rule;
     2. structural overrides, only where V2 genuinely changes the shape
        (cards, grid, navbar, hero, ranking).

   Two tokens escape the remap: --c-primary and --c-primary-light are
   set inline on <html> by web/js/theme.js, which no rule can beat
   without !important. That is deliberate: the accent must stay the
   organization's own color (GET /branding). Only dark mode rewrites
   them, with !important, to pick up the lightened variant theme.js
   computes.
   ========================================================= */

/* ---------------------------------------------------------
   1. Jetons — mode clair
   --------------------------------------------------------- */
html[data-ui="v2"]{
  color-scheme:light;

  /* neutres chauds : le teal de marque ne baigne plus l'interface,
     il ne sert plus que d'accent */
  --c-text-primary:#0C0A09;
  --c-text-secondary:#57534E;
  --c-background:#0C0A09;
  --c-white:#FFFFFF;
  --c-white-2:#FAFAF9;
  --c-grey-border:#E7E5E4;
  --c-grey-light:#EDEBE9;
  --c-grey-lightest:#F5F5F4;
  --c-grey-darkest:#A8A29E;

  --c-green:#16A34A;
  --c-green-dark:#15803D;
  --c-red:#E11D48;
  --c-orange:#B45309;
  --c-purple:#7C3AED;

  --c-green-soft:rgba(22,163,74,.10);
  --c-green-text:#15803D;
  --c-orange-soft:rgba(180,83,9,.10);
  --c-orange-text:#9A4B08;
  --c-red-soft:rgba(225,29,72,.09);
  --c-red-text:#BE123C;
  --c-purple-soft:rgba(124,58,237,.10);
  --c-gold-soft:#FBF3E0;
  --c-gold-text:#8A6314;
  --c-cat-text:#44403C;

  /* two very low layers: V2 uses hairlines rather than shadows, the
     shadow only serving hover states and floating layers */
  --shadow-nav:0 1px 2px rgba(12,10,9,.04);
  --shadow-drop:0 2px 8px rgba(12,10,9,.05), 0 16px 40px rgba(12,10,9,.10);
  --shadow-hover:0 2px 6px rgba(12,10,9,.05), 0 18px 44px rgba(12,10,9,.12);

  --r-input:10px;
  --r-btn:10px;
  --r-card:16px;

  --fs-body:15px;
  --fs-small:14px;
  --fs-xsmall:13px;
  --fs-section:17px;
  --fs-page:26px;

  --nav-h:68px;

  /* ----- tokens specific to V2 ----- */
  --v2-ink:#0C0A09;              /* the "black" fill, for neutral active states */
  --v2-on-ink:#FFFFFF;
  --v2-surface-2:#F5F5F4;
  --v2-hairline:#E7E5E4;
  --v2-gold:#B4881F;
  --v2-glass:rgba(255,255,255,.72);
  --v2-glass-strong:rgba(255,255,255,.92);
  --v2-on-media:#FFFFFF;
  --v2-media-scrim:rgba(12,10,9,.55);

  --v2-r-media:16px;
  --v2-r-modal:20px;

  /* Border of an input control. The decorative hairline
     (--v2-hairline) sits at 1.26:1, which is enough to separate two
     blocks but not to say "there is something to fill in here": on a
     field the border is the only affordance, and WCAG 1.4.11 asks for
     3:1 on what identifies a component. 3.65:1 on white, 3.50:1 on the
     page background. */
  --v2-control-border:#8A8580;
  /* Surface of an element raised above another (the selected pill of a
     segmented control). In light mode that is white; in dark the remap
     makes --c-white #141416, which is DARKER than the track it sits on
     — the selected state then read as the unselected one. */
  --v2-raised:#FFFFFF;
  --v2-raised-ring:transparent;

  /* spinner sitting on a colored fill (primary button, color swatch):
     it has to follow the button's ink, which turns dark in dark mode */
  --v2-spin-ink:#FFFFFF;
  --v2-spin-track:rgba(255,255,255,.35);

  --v2-ease:cubic-bezier(.2,.7,.2,1);
  --v2-dur-1:150ms;
  --v2-dur-2:220ms;
  --v2-dur-3:320ms;

  /* the dark palette, declared here so it exists only once: the two
     dark-mode selectors below merely wire the tokens to it */
  --d-text-primary:#FAFAF9;
  --d-text-secondary:#A8A29E;
  --d-surface:#141416;
  --d-surface-2:#1C1C1F;
  --d-page:#0B0B0C;
  --d-hairline:#2A2A2E;
  --d-hairline-strong:#35353A;
  --d-muted:#78716C;
}

/* ---------------------------------------------------------
   2. Tokens — dark mode

   Two ways in: the explicit "Sombre" choice, and "Auto" (no data-theme
   attribute) which follows the system setting. The values live in the
   --d-* above, these blocks only wire them up.
   --------------------------------------------------------- */
html[data-ui="v2"][data-theme="dark"]{
  color-scheme:dark;

  --c-text-primary:var(--d-text-primary);
  --c-text-secondary:var(--d-text-secondary);
  --c-white:var(--d-surface);
  --c-white-2:var(--d-page);
  --c-grey-border:var(--d-hairline);
  --c-grey-light:var(--d-hairline);
  --c-grey-lightest:var(--d-surface-2);
  --c-grey-darkest:var(--d-muted);
  --c-background:#000000;

  --c-green:#4ADE80;
  --c-green-dark:#4ADE80;
  --c-red:#FB7185;
  --c-orange:#FBBF24;
  --c-purple:#C4B5FD;

  --c-green-soft:rgba(74,222,128,.14);
  --c-green-text:#86EFAC;
  --c-orange-soft:rgba(251,191,36,.14);
  --c-orange-text:#FCD34D;
  --c-red-soft:rgba(251,113,133,.14);
  --c-red-text:#FDA4AF;
  --c-purple-soft:rgba(196,181,253,.14);
  --c-gold-soft:rgba(212,175,88,.14);
  --c-gold-text:#E7C878;
  --c-cat-text:#D6D3D1;

  --shadow-nav:0 1px 2px rgba(0,0,0,.5);
  --shadow-drop:0 2px 8px rgba(0,0,0,.5), 0 16px 40px rgba(0,0,0,.55);
  --shadow-hover:0 2px 6px rgba(0,0,0,.5), 0 18px 44px rgba(0,0,0,.6);

  --v2-ink:#FAFAF9;
  --v2-on-ink:#0C0A09;
  --v2-surface-2:var(--d-surface-2);
  --v2-hairline:var(--d-hairline);
  --v2-gold:#E7C878;
  --v2-glass:rgba(20,20,22,.72);
  --v2-glass-strong:rgba(20,20,22,.94);
  --v2-media-scrim:rgba(0,0,0,.55);
  --v2-spin-ink:#0C0A09;
  --v2-spin-track:rgba(12,10,9,.38);
  --v2-control-border:#63636A;
  --v2-raised:var(--d-hairline-strong);
  --v2-raised-ring:rgba(250,250,249,.22);

  /* the only !important in the file. theme.js writes --c-primary inline
     on <html>: this is the only way to pick up, in dark mode, the
     lightened variant it computes (--c-primary-on-dark), without
     copying out the ~16 selectors that consume the accent. */
  --c-primary:var(--c-primary-on-dark, #4FC3DE) !important;
  --c-primary-light:var(--c-primary-on-dark-light, rgba(79,195,222,.16)) !important;
}

@media (prefers-color-scheme:dark){
  html[data-ui="v2"]:not([data-theme="light"]){
    color-scheme:dark;

    --c-text-primary:var(--d-text-primary);
    --c-text-secondary:var(--d-text-secondary);
    --c-white:var(--d-surface);
    --c-white-2:var(--d-page);
    --c-grey-border:var(--d-hairline);
    --c-grey-light:var(--d-hairline);
    --c-grey-lightest:var(--d-surface-2);
    --c-grey-darkest:var(--d-muted);
    --c-background:#000000;

    --c-green:#4ADE80;
    --c-green-dark:#4ADE80;
    --c-red:#FB7185;
    --c-orange:#FBBF24;
    --c-purple:#C4B5FD;

    --c-green-soft:rgba(74,222,128,.14);
    --c-green-text:#86EFAC;
    --c-orange-soft:rgba(251,191,36,.14);
    --c-orange-text:#FCD34D;
    --c-red-soft:rgba(251,113,133,.14);
    --c-red-text:#FDA4AF;
    --c-purple-soft:rgba(196,181,253,.14);
    --c-gold-soft:rgba(212,175,88,.14);
    --c-gold-text:#E7C878;
    --c-cat-text:#D6D3D1;

    --shadow-nav:0 1px 2px rgba(0,0,0,.5);
    --shadow-drop:0 2px 8px rgba(0,0,0,.5), 0 16px 40px rgba(0,0,0,.55);
    --shadow-hover:0 2px 6px rgba(0,0,0,.5), 0 18px 44px rgba(0,0,0,.6);

    --v2-ink:#FAFAF9;
    --v2-on-ink:#0C0A09;
    --v2-surface-2:var(--d-surface-2);
    --v2-hairline:var(--d-hairline);
    --v2-gold:#E7C878;
    --v2-glass:rgba(20,20,22,.72);
    --v2-glass-strong:rgba(20,20,22,.94);
    --v2-media-scrim:rgba(0,0,0,.55);
    --v2-spin-ink:#0C0A09;
    --v2-spin-track:rgba(12,10,9,.38);
    --v2-control-border:#63636A;
    --v2-raised:var(--d-hairline-strong);
    --v2-raised-ring:rgba(250,250,249,.22);

    --c-primary:var(--c-primary-on-dark, #4FC3DE) !important;
    --c-primary-light:var(--c-primary-on-dark-light, rgba(79,195,222,.16)) !important;
  }
}

/* ---------------------------------------------------------
   3. Compact density
   Shrinks the spacing scale, never the touch targets: button, chip
   and field heights do not move.
   --------------------------------------------------------- */
html[data-ui="v2"][data-density="compact"]{
  --sp-3:10px;
  --sp-4:12px;
  --sp-5:16px;
  --sp-6:18px;
  --sp-8:24px;
  --gap:14px;
  --nav-h:60px;
  --v2-grid-min:220px;
}

/* ---------------------------------------------------------
   4. Base : typographie et fond de page
   --------------------------------------------------------- */
html[data-ui="v2"] body{
  background:var(--c-white-2);
  line-height:1.55;
  letter-spacing:-0.006em;
}

/* The navbar is position:fixed: without this reserve, an element
   reached by keyboard (tabbing) or through an anchor lands at the top
   of the scroll — that is, under the bar. WCAG 2.2 "Focus Not
   Obscured" (AA) requires the focus to stay at least partly visible. */
html[data-ui="v2"]{ scroll-padding-top:calc(var(--nav-h) + var(--demo-h) + var(--sp-3)); }

/* index.html declares viewport-fit=cover, but the only env() in the
   project are two padding-bottom: in landscape on a notched phone the
   first column of the feed and the navbar ran under the cutout. */
html[data-ui="v2"] .container{
  padding-left:max(var(--sp-5), env(safe-area-inset-left));
  padding-right:max(var(--sp-5), env(safe-area-inset-right));
}
@media (max-width:767px){
  html[data-ui="v2"] .container{
    padding-left:max(var(--sp-4), env(safe-area-inset-left));
    padding-right:max(var(--sp-4), env(safe-area-inset-right));
  }
}

/* 100vh is the viewport with the mobile browser bars retracted: on a
   phone it is taller than what is actually visible, so the login screen
   and the app shell always ended a little below the fold. */
@supports (min-height:100dvh){
  html[data-ui="v2"] .app-main,
  html[data-ui="v2"] .auth{ min-height:100dvh; }
}

html[data-ui="v2"] h1,
html[data-ui="v2"] h2,
html[data-ui="v2"] h3,
html[data-ui="v2"] h4{ letter-spacing:-0.028em; }

/* the screen title is the one place where V2 scales up sharply: that
   is what sets the editorial rhythm, without touching body text or
   the components */
html[data-ui="v2"] h1{
  font-size:clamp(26px, 3.2vw, 36px);
  font-weight:700;
  line-height:1.12;
}
html[data-ui="v2"] h2{ font-size:clamp(20px, 2.2vw, 24px); }

html[data-ui="v2"] .page-head{ margin-bottom:var(--sp-6); }
html[data-ui="v2"] .page-head .sub{
  margin-top:10px;
  font-size:var(--fs-body);
  max-width:60ch;
}

html[data-ui="v2"] .app-main{ padding-top:calc(var(--nav-h) + var(--demo-h) + 36px); }

html[data-ui="v2"] a:hover{ text-decoration:none; opacity:.78; }

/* ---------------------------------------------------------
   5. Sections, boutons, badges, chips
   --------------------------------------------------------- */
html[data-ui="v2"] .sc-section{
  border:1px solid var(--v2-hairline);
  box-shadow:none;
}
html[data-ui="v2"] .sc-section__head{
  padding:var(--sp-5) var(--sp-5) var(--sp-4);
  border-bottom-color:var(--v2-hairline);
}
html[data-ui="v2"] .sc-section__title{
  font-size:var(--fs-section);
  font-weight:600;
  letter-spacing:-0.02em;
}

html[data-ui="v2"] .btn{
  height:44px;
  padding:0 20px;
  font-weight:600;
  transition:background var(--v2-dur-1) var(--v2-ease),
             color var(--v2-dur-1) var(--v2-ease),
             box-shadow var(--v2-dur-1) var(--v2-ease),
             transform var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .btn:hover{ filter:none; }
html[data-ui="v2"] .btn:active{ transform:scale(.98); }
html[data-ui="v2"] .btn--sm{ height:36px; padding:0 14px; }

html[data-ui="v2"] .btn--primary:hover{ box-shadow:0 6px 18px var(--c-primary-light); }
html[data-ui="v2"] .btn--outline{ box-shadow:inset 0 0 0 1px var(--c-grey-border); }
html[data-ui="v2"] .btn--grey{
  background:var(--c-white);
  box-shadow:inset 0 0 0 1px var(--c-grey-border);
}
html[data-ui="v2"] .btn--grey:hover{ background:var(--c-grey-lightest); }

/* V1 uses a transparent outlined secondary button; in V2 the login
   screen's fields and buttons sit on a solid surface */
html[data-ui="v2"] .btn--secondary{
  background:var(--c-white);
  color:var(--c-text-primary);
  box-shadow:inset 0 0 0 1px var(--c-grey-border);
}
html[data-ui="v2"] .btn--secondary:hover{ background:var(--c-grey-lightest); }

/* V2's green (#16A34A) only carries 3.29:1 against white: the
   "Valider" label of a moderation button is normal text, so it needs
   4.5:1. The darker hue (#15803D) reaches 5.02:1, and dark mode points
   both tokens at the same light green, whose ink is dark — nothing
   changes there. */
html[data-ui="v2"] .btn--green{ background:var(--c-green-dark); }

/* The reject button's border is hardcoded to rgba(243,70,70,.25) in
   V1: that is the old red, it follows neither V2's red nor dark mode,
   where it vanishes. Derived from the button's ink instead. */
html[data-ui="v2"] .btn--red{
  box-shadow:inset 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
}

/* .btn--light is the tertiary action (download the original,
   promote/demote): tinted with the accent, it visually competed with
   the row's primary action. In V2 the accent is reserved for a
   screen's primary action, everything else is neutral. The lightbox
   panel keeps its light variant, further down. */
html[data-ui="v2"] .btn--light{
  background:var(--v2-surface-2);
  color:var(--c-text-primary);
  box-shadow:inset 0 0 0 1px var(--v2-hairline);
}
html[data-ui="v2"] .btn--light:hover{ background:var(--c-grey-light); }

html[data-ui="v2"] .badge{
  height:26px;
  font-size:var(--fs-xsmall);
  font-weight:600;
  letter-spacing:-0.01em;
}
html[data-ui="v2"] .cat-tag{
  background:transparent;
  box-shadow:inset 0 0 0 1px var(--c-grey-border);
  color:var(--c-text-secondary);
  font-weight:500;
}
/* A category tag standing next to a status badge has to be the same
   pill: .cat-tag is 22px tall and 12px of text against the badge's 26
   and 13, so the two staggered on every mixed row — lightbox panel,
   moderation item, ranking row. Scoped to those rows: on a card's foot
   the tag stands alone, with nothing to line up against. */
html[data-ui="v2"] .lightbox__tags .cat-tag,
html[data-ui="v2"] .mod-item__tags .cat-tag,
html[data-ui="v2"] .rank-row .cat-tag{
  height:26px;
  font-size:var(--fs-xsmall);
  letter-spacing:-0.01em;
}

html[data-ui="v2"] .chip{
  height:38px;
  padding:0 16px;
  font-weight:500;
  transition:background var(--v2-dur-1) var(--v2-ease),
             border-color var(--v2-dur-1) var(--v2-ease),
             color var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .chip:hover{
  border-color:var(--c-text-primary);
  color:var(--c-text-primary);
}
/* active state as a neutral fill rather than the accent: the brand
   color stays reserved for the primary action, and a filter is not
   one */
html[data-ui="v2"] .chip.is-active{
  background:var(--v2-ink);
  border-color:var(--v2-ink);
  color:var(--v2-on-ink);
  font-weight:600;
}
/* .6 on --c-text-secondary composites to #9A9795, i.e. 2.87:1 for a
   12px label — below AA, and a regression on the V1 which was at .75.
   The count is already quiet enough at 12px; on the active chip it
   simply follows the pill's ink. */
html[data-ui="v2"] .chip__n{ opacity:1; color:var(--c-text-secondary); font-variant-numeric:tabular-nums; }
html[data-ui="v2"] .chip.is-active .chip__n{ color:inherit; opacity:.7; }

/* ---------------------------------------------------------
   6. Champs
   --------------------------------------------------------- */
/* 16px and no less on anything typed into: below that, Safari iOS
   zooms into the field on focus and never zooms back out — the user is
   left stuck in a magnified page after entering their address or their
   caption. The 15px of body text stays the rule everywhere else. */
html[data-ui="v2"] .input,
html[data-ui="v2"] .select,
html[data-ui="v2"] .textarea{
  height:46px;
  padding:0 15px;
  /* background-color, never the shorthand: .select carries its chevron
     as a background-image positioned once at the right edge
     (components.css), and the shorthand resets background-repeat and
     background-position — the chevron was tiled across the whole field
     and the value underneath was unreadable. Same trap as .thumb and
     .lightbox__stage, which carry the blur placeholder. */
  background-color:var(--c-white);
  color:var(--c-text-primary);
  font-size:16px;
  /* the hairline the rest of the V2 uses is decorative; on a field it
     is the only thing that says the control exists (see
     --v2-control-border) */
  border-color:var(--v2-control-border);
}
html[data-ui="v2"] .search{ border-color:var(--v2-control-border); }
/* off state of a switch: the track and its knob were both drawn in the
   decorative hairline, 1.26:1 against the surface and against each
   other — the control was all but invisible until it was turned on */
html[data-ui="v2"] .switch__track{
  background:var(--c-grey-lightest);
  box-shadow:inset 0 0 0 1px var(--v2-control-border);
}
html[data-ui="v2"] .textarea{ height:auto; padding:13px 15px; min-height:96px; }
html[data-ui="v2"] .input:focus,
html[data-ui="v2"] .select:focus,
html[data-ui="v2"] .textarea:focus{
  box-shadow:0 0 0 3px var(--c-primary-light);
}
html[data-ui="v2"] .field label{ font-size:var(--fs-small); font-weight:600; }

/* the .select chevron is a data-URI SVG whose color is baked into the
   URL (stroke='%23605E74'): unreadable in dark mode. Rewritten with a
   grey that holds on both backgrounds. The uniform padding set just
   above overrides .select's padding-right: it is restored here, or the
   value slides under the chevron. */
html[data-ui="v2"] .select{
  padding-right:38px;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5L6 6.5L11 1.5' stroke='%23A8A29E' stroke-width='1.7' fill='none' stroke-linecap='round'/></svg>");
  /* belt and braces: whatever rule ends up winning on the background
     above, the chevron is drawn once, at the right edge */
  background-repeat:no-repeat;
  background-position:right 14px center;
}

html[data-ui="v2"] .checkbox-chip{ height:38px; }
html[data-ui="v2"] .switch__track{ background:var(--c-grey-border); }

/* ---------------------------------------------------------
   7. Navbar
   A translucent bar laid over the content, hairline rather than
   shadow.
   --------------------------------------------------------- */
html[data-ui="v2"] .navbar{
  background:var(--v2-glass);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  backdrop-filter:blur(20px) saturate(180%);
  border-bottom:1px solid var(--v2-hairline);
  box-shadow:none;
}
/* without backdrop-filter (older Firefox, embedded browsers), the
   translucency would leave the content unreadable under the bar */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  html[data-ui="v2"] .navbar{ background:var(--c-white); }
}

html[data-ui="v2"] .brand{ font-size:16px; letter-spacing:-0.03em; }
html[data-ui="v2"] .brand__logo{ height:26px; }

html[data-ui="v2"] .nav-link{
  height:38px;
  padding:0 13px;
  font-weight:500;
  border-radius:var(--r-pill);
}
html[data-ui="v2"] .nav-link:hover{ background:var(--v2-surface-2); }
html[data-ui="v2"] .nav-link.is-active{
  color:var(--c-text-primary);
  font-weight:600;
}
/* the pill itself, cf .nav-link__ind in layout.css: the background of
   the current entry lives on that node so it can slide */
html[data-ui="v2"] .nav-link__ind{ background:var(--v2-surface-2); }
html[data-ui="v2"] .nav-link__count{
  background:var(--c-red);
  /* same reason as the toasts: white on the light red of the dark
     scheme sat at 2.69:1 */
  color:var(--v2-on-ink);
  font-variant-numeric:tabular-nums;
}

html[data-ui="v2"] .avatar{
  border-radius:var(--r-pill);
  background:var(--v2-ink);
  color:var(--v2-on-ink);
  font-weight:600;
}
/* the rule above is more specific than .avatar--pending, so the skeleton
   disc would come out as a filled avatar with no initials in it */
html[data-ui="v2"] .avatar--pending{
  background:linear-gradient(100deg,
    var(--c-grey-lightest) 30%, var(--c-grey-light) 50%, var(--c-grey-lightest) 70%);
  background-size:200% 100%;
}
html[data-ui="v2"] .user-btn{ border-radius:var(--r-pill); padding:var(--sp-1) 10px var(--sp-1) var(--sp-1); }
html[data-ui="v2"] .user-btn:hover{ background:var(--v2-surface-2); }

html[data-ui="v2"] .dropdown{
  border:1px solid var(--v2-hairline);
  border-radius:14px;
}
html[data-ui="v2"] .dropdown__item{ font-weight:500; }
html[data-ui="v2"] .dropdown__item:hover{ background:var(--v2-surface-2); }

/* appearance switch embedded in the profile menu */
html[data-ui="v2"] .dropdown .view-toggle{ width:100%; }
html[data-ui="v2"] .dropdown .view-toggle button{ flex:1 1 0; }

/* ---------------------------------------------------------
   8. Feed grid and cards

   V2's central move: the card drops its chrome (background, border,
   shadow) and keeps only the photo, the caption and the author. The
   elevation moves onto the thumbnail alone, on hover.
   --------------------------------------------------------- */
html[data-ui="v2"] .grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(var(--v2-grid-min, 272px), 1fr));
  gap:var(--sp-8) var(--gap);
}
/* cancels the per-column widths set by layout.css's media queries: the
   grid is what decides the number of columns.
   display:flex rather than the default block: the card is height:100%,
   which only resolves against a parent of definite height. The grid
   item is indeed stretched by align-items:stretch, but its height stays
   "auto" as far as percentage resolution goes — without this flex,
   .card__foot{margin-top:auto} has no free space to consume and the
   card feet in a row do not line up. */
html[data-ui="v2"] .grid > .grid-item{ width:auto; display:flex; }
html[data-ui="v2"] .grid > .grid-item > .card{ flex:1 1 auto; }

html[data-ui="v2"] .card{
  /* the vertical spacing is set explicitly further down (.card__top
     padding, .card__foot auto margin): a gap here would add to those
     values and make the rhythm impossible to read */
  gap:0;
  background:transparent;
  border:0;
  border-radius:0;
  overflow:visible;
  box-shadow:none;
}
html[data-ui="v2"] .card:hover,
html[data-ui="v2"] .card:focus-within{ box-shadow:none; }

html[data-ui="v2"] .thumb{
  padding-top:0;
  aspect-ratio:4 / 3;
  border-radius:var(--v2-r-media);
  /* background-color, not the background shorthand: .thumb--blur
     (components.css) sets background-size:cover to spread the 20px
     blurred placeholder over the whole thumbnail, and the shorthand
     resets that value — the thumbnail then showed the placeholder at
     its native size, tiled. Same trap further down on
     .lightbox__stage. */
  background-color:var(--v2-surface-2);
  box-shadow:0 0 0 1px var(--v2-hairline);
  transition:box-shadow var(--v2-dur-2) var(--v2-ease);
}
/* safety net: the placeholder is set inline, and must never tile
   whichever rule wins above */
html[data-ui="v2"] .thumb--blur,
html[data-ui="v2"] .mini-thumb--blur{
  background-size:cover;
  background-position:center;
  background-repeat:no-repeat;
}
html[data-ui="v2"] .card:hover .thumb,
html[data-ui="v2"] .card:focus-within .thumb{
  box-shadow:0 0 0 1px var(--v2-hairline), var(--shadow-hover);
}
html[data-ui="v2"] .thumb img{ transition:transform var(--v2-dur-3) var(--v2-ease); }
html[data-ui="v2"] .card__open:hover .thumb img{ transform:scale(1.03); }

/* Rhythm of the text block, in the order the eye reads it:
   photo -> 12px -> caption -> 5px -> author -> 10px -> foot.
   V1 put that first gap in .card__top's padding; V2 had set it to 0 to
   drop the side padding, and the caption ended up glued to the photo.
   The caption/author gap is 5px rather than 3px now that the caption no
   longer reserves an empty second line: at 3px the two lines touched. */
html[data-ui="v2"] .card__top{ padding:12px 0 0; gap:5px; }
html[data-ui="v2"] .card__caption{
  font-size:var(--fs-body);
  font-weight:600;
  letter-spacing:-0.015em;
  line-height:1.3;
  /* Two lines at most: a 120-character caption (the maximum accepted on
     submission) would otherwise take five lines and impose its height on
     the whole row. The full text stays readable in the lightbox.

     No floor of two lines any more. It was reserving an empty second line
     under every short caption, which put roughly 23px between the title
     and its author — the two read as unrelated blocks rather than as one
     credit. The card feet still line up: .card__foot{margin-top:auto}
     (below) is what does that, not this reserve. */
  display:-webkit-box;
  -webkit-box-orient:vertical;
  -webkit-line-clamp:2;
  line-clamp:2;
  overflow:hidden;
  overflow-wrap:anywhere;
}
/* An author name or a category with no space in it is wider than the
   card in compact density (266px); nothing in the project sets
   overflow-wrap, and .cat-tag is nowrap, so both spilled out of a card
   that is overflow:visible — and out of the page in the last column. */
html[data-ui="v2"] .card__meta{ font-size:var(--fs-xsmall); overflow-wrap:anywhere; }
html[data-ui="v2"] .card__foot .cat-tag{ white-space:normal; max-width:100%; }
/* margin-top:auto (a V1 rule) is what lines up the card feet: V2 was
   overriding it with a fixed margin */
html[data-ui="v2"] .card__foot{ padding:10px 0 0; margin-top:auto; }

/* badge laid over the photo: a glass pill, readable whatever the
   image underneath */
html[data-ui="v2"] .thumb__badge{
  top:12px;
  left:12px;
  background:var(--v2-glass-strong);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  backdrop-filter:blur(10px) saturate(160%);
  color:var(--c-text-primary);
  box-shadow:0 2px 10px rgba(0,0,0,.18);
}
html[data-ui="v2"] .thumb__badge--right{ left:auto; right:12px; }

html[data-ui="v2"] .like-btn{
  position:relative;
  height:34px;
  padding:0 12px;
  border-color:transparent;
  background:transparent;
  color:var(--c-text-secondary);
  transition:color var(--v2-dur-1) var(--v2-ease),
             background var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .like-btn{ --burst-ink:var(--c-red); }
html[data-ui="v2"] .like-btn:hover{ background:var(--v2-surface-2); color:var(--c-text-primary); }
/* Liked: only the ink changes. V2 used to put a solid red pill here,
   and since about one card in three is liked, the grid became speckled
   with color that pulled the eye before the photos did. The pill stays
   for hover, where it acts as feedback. */
html[data-ui="v2"] .like-btn.is-liked{
  background:transparent;
  color:var(--c-red);
}
html[data-ui="v2"] .like-btn.is-liked:hover{ background:var(--c-red-soft); color:var(--c-red); }
html[data-ui="v2"] .like-btn:disabled{ background:transparent; }

/* The card foot must line up with the edges of the photo. On the left
   the category pill already does; on the right the like was inset by
   its own 12px of padding, leaving the counter floating inside the
   frame. The negative margin cancels that padding without shrinking the
   clickable area. */
html[data-ui="v2"] .card__foot .like-btn,
html[data-ui="v2"] .card__foot .rank-likes{ margin-right:-12px; padding-right:12px; }
/* the target stays 34px tall so the foot keeps its balance, but its
   touch area is raised to 44px without shifting the drawing */
html[data-ui="v2"] .card__foot .like-btn::before{
  content:'';
  position:absolute;
  inset:-5px 0;
}

html[data-ui="v2"] .card--skeleton .thumb{ box-shadow:none; }
html[data-ui="v2"] .sk-bar{ border-radius:6px; }

/* ---------------------------------------------------------
   9. Barre d'outils du fil : filtres et recherche
   --------------------------------------------------------- */
html[data-ui="v2"] .feed-tools{ margin-bottom:var(--sp-6); align-items:center; }
html[data-ui="v2"] .search{
  height:44px;
  padding:0 var(--sp-4);
  border-width:1px;
  transition:border-color var(--v2-dur-1) var(--v2-ease),
             box-shadow var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .search:focus-within{ box-shadow:0 0 0 3px var(--c-primary-light); }
html[data-ui="v2"] .search__input{ font-size:16px; }

/* The voting status line. V2 strips it of its frame: no longer a
   block but a caption, under the screen title. */
html[data-ui="v2"] .quota{
  padding:0;
  margin-bottom:var(--sp-5);
  background:transparent;
  box-shadow:none;
  border-radius:0;
  font-size:var(--fs-small);
  color:var(--c-text-secondary);
  gap:var(--sp-2);
}
/* .quota__txt is a flex item with an "auto" basis, so its hypothetical
   size is that of the whole sentence — and it is on that size that the
   browser decides the line breaks, before even considering shrinking
   the item. On mobile the heart was therefore alone on the first line,
   the sentence on the second, the period on the third. With basis 0 the
   item always fits on the line, then spreads over the remaining room
   and breaks its own lines. */
html[data-ui="v2"] .quota__txt{
  min-width:0;
  flex:1 1 0;
  /* the voting rule is the information, the period is the context: one
     takes the primary ink, the other stays grey */
  color:var(--c-text-primary);
}
html[data-ui="v2"] .quota > svg{ align-self:flex-start; margin-top:3px; }
html[data-ui="v2"] .quota__rule{ color:var(--c-text-secondary); flex:0 0 auto; }

html[data-ui="v2"] .prize{
  /* the only block of the feed still on a hardcoded 18px bottom margin,
     where everything around it is on --sp-6 */
  margin-bottom:var(--sp-6);
  padding:var(--sp-5) var(--sp-6);
  border:1px solid var(--v2-hairline);
  border-left:1px solid var(--v2-hairline);
  border-radius:var(--r-card);
  box-shadow:none;
  gap:var(--sp-2);
}
html[data-ui="v2"] .prize--pending{
  background:var(--v2-surface-2);
  border-color:transparent;
}
/* same size as .sc-section__title, so the same weight: the V1 sets 700
   here and 600 there, which read as two different levels */
html[data-ui="v2"] .prize__title{ font-size:var(--fs-section); font-weight:600; letter-spacing:-0.02em; }
/* On a wide screen the block spans the whole container, about 160
   characters per line: two to three times the length past which the eye
   loses the start of the next line. The frame keeps its width, the text
   does not. */
html[data-ui="v2"] .prize p{ max-width:68ch; }

/* ---------------------------------------------------------
   10. Winning photo banner
   An editorial hero: fixed ratio, neutral veil, display title.
   --------------------------------------------------------- */
html[data-ui="v2"] .winner{
  min-height:0;
  aspect-ratio:16 / 7;
  border-radius:var(--v2-r-modal);
  box-shadow:none;
  align-items:flex-end;
}
html[data-ui="v2"] .winner-row--multi .winner{ min-height:0; aspect-ratio:4 / 3; }

/* The scrim has to hold the caption over any photo, including a bright
   sky. At the old 42% stop it only reached .30, which puts white text
   at 2.03:1 on a white photo — and that is exactly where a three-line
   title lands on a phone, where the banner is tall and the text block
   starts high. The ramp now reaches .55 before the content zone and
   .88 at the foot. */
html[data-ui="v2"] .winner__veil,
html[data-ui="v2"] .winner-row--multi .winner__veil{
  background:linear-gradient(180deg,
    rgba(12,10,9,0) 0%,
    rgba(12,10,9,.14) 28%,
    rgba(12,10,9,.55) 58%,
    rgba(12,10,9,.88) 100%);
}
html[data-ui="v2"] .winner__content{
  justify-content:flex-end;
  padding:var(--sp-8);
  max-width:720px;
  color:var(--v2-on-media);
}
html[data-ui="v2"] .winner__title{
  font-size:clamp(24px, 3.4vw, 40px);
  letter-spacing:-0.03em;
  line-height:1.08;
  /* second line of defence behind the scrim: a wide, low-opacity halo
     is invisible on a dark photo and buys the missing stop of contrast
     on a bright one */
  text-shadow:0 1px 28px rgba(12,10,9,.55);
}
html[data-ui="v2"] .winner-row--multi .winner__title{ font-size:clamp(20px, 2.4vw, 26px); }
html[data-ui="v2"] .winner__label,
html[data-ui="v2"] .podium-card__label{
  background:var(--v2-glass-strong);
  color:var(--c-text-primary);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  backdrop-filter:blur(10px) saturate(160%);
  letter-spacing:.06em;
}
html[data-ui="v2"] .winner__pill{
  background:rgba(255,255,255,.16);
  -webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);
  color:#FFFFFF;
}
/* The heart stays red, like every other like count (cf .rank-likes,
   .podium-card__likes): white, it read as one more piece of the label
   rather than as the vote count. A fixed value and not --c-red: the pill
   sits on the dark veil of a photo in both themes, where the light
   theme's deeper red loses too much against the image. */
html[data-ui="v2"] .winner__pill svg{ color:#FB7185; }
html[data-ui="v2"] .winner__meta{ color:rgba(255,255,255,.86); }

html[data-ui="v2"] .winner{ animation:v2-rise var(--v2-dur-3) var(--v2-ease) both; }
@keyframes v2-rise{
  from{ opacity:0; transform:translateY(10px); }
  to{ opacity:1; transform:none; }
}

/* ---------------------------------------------------------
   10 bis. Palmarès

   Le podium est le même hero, en plus haut : il est seul sur son
   écran, il n'a pas la grille des photos sous lui pour l'équilibrer.
   Le voile reprend exactement la rampe corrigée du bandeau, pour la
   même raison de contraste sur une photo claire.
   --------------------------------------------------------- */
html[data-ui="v2"] .podium-card{
  min-height:0;
  aspect-ratio:16 / 9;
  border-radius:var(--v2-r-modal);
  box-shadow:none;
  animation:v2-rise var(--v2-dur-3) var(--v2-ease) both;
}
html[data-ui="v2"] .podium-top--multi .podium-card{ aspect-ratio:4 / 3; }
html[data-ui="v2"] .podium-card__veil{
  background:linear-gradient(180deg,
    rgba(12,10,9,0) 0%,
    rgba(12,10,9,.14) 28%,
    rgba(12,10,9,.55) 58%,
    rgba(12,10,9,.88) 100%);
}
html[data-ui="v2"] .podium-card__content{
  padding:var(--sp-8);
  max-width:720px;
  color:var(--v2-on-media);
}
html[data-ui="v2"] .podium-card__title{
  font-size:clamp(26px, 3.8vw, 44px);
  letter-spacing:-0.03em;
  line-height:1.08;
  text-shadow:0 1px 28px rgba(12,10,9,.55);
}
html[data-ui="v2"] .podium-top--multi .podium-card__title{ font-size:clamp(20px, 2.4vw, 28px); }
/* The winning pill is the one moving thing on the hero: a gold sheen
   crosses the glass, so the badge reads as a prize rather than as one
   more label. Both heroes carry it — the feed banner and the podium
   show the same photo under the same word, they must not differ. It
   stops there: on the ranking thumbnails (.thumb__badge) a dozen pills
   sweeping at once would read as a broken page, not as a prize.
   A pseudo-element and not an animated background: App.morph rebuilds
   this markup from a string, and an inline style would be wiped
   mid-sweep (same reason as burst.js). */
html[data-ui="v2"] .winner__label,
html[data-ui="v2"] .podium-card__label{
  position:relative;
  overflow:hidden;
}
html[data-ui="v2"] .winner__label::after,
html[data-ui="v2"] .podium-card__label::after{
  content:"";
  position:absolute;
  inset:0;
  pointer-events:none;
  background:linear-gradient(100deg,
    rgba(180,136,31,0) 38%,
    rgba(180,136,31,.42) 50%,
    rgba(180,136,31,0) 62%);
  transform:translateX(-100%);
  animation:v2-label-sheen 4.5s var(--v2-ease) infinite;
}
@keyframes v2-label-sheen{
  /* the pause is inside the keyframes, not between two runs: the sweep
     itself lasts a third of the cycle, the rest is the pill at rest */
  0%{ transform:translateX(-100%); }
  35%, 100%{ transform:translateX(100%); }
}
html[data-ui="v2"] .podium-card__likes{
  background:rgba(255,255,255,.16);
  -webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);
  color:#FFFFFF;
}
html[data-ui="v2"] .podium-card__meta{ color:rgba(255,255,255,.86); }

html[data-ui="v2"] .reveal{
  border-radius:var(--v2-r-modal);
  animation:v2-rise var(--v2-dur-3) var(--v2-ease) both;
}

/* la course : mêmes surfaces que le reste de la V2, la géométrie
   (hauteurs, largeurs) reste celle de layout.css puisque race.js la
   mesure au lancement */
html[data-ui="v2"] .race-bar{ background:var(--v2-hairline); }
html[data-ui="v2"] .race-bar__fill{ background:var(--c-primary); }
html[data-ui="v2"] .race-thumb{ background:var(--v2-hairline); }
html[data-ui="v2"] .race-main b{ letter-spacing:-0.01em; }
html[data-ui="v2"] .race-count{ font-variant-numeric:tabular-nums; }

/* ---------------------------------------------------------
   11. Classement
   --------------------------------------------------------- */
html[data-ui="v2"] .stat-row{ gap:var(--sp-3); margin-bottom:var(--sp-6); }
html[data-ui="v2"] .stat{
  padding:var(--sp-5);
  border:1px solid var(--v2-hairline);
  box-shadow:none;
}
html[data-ui="v2"] .stat b{
  font-size:clamp(22px, 2.4vw, 28px);
  letter-spacing:-0.03em;
  font-variant-numeric:tabular-nums;
  /* One of the four tiles carries not a number but a spelled-out date
     ("18 septembre 2026", see viewRanking): it wraps to two lines as
     soon as the row tightens. Tight leading and balanced wrapping so
     those two lines stay one block, rather than an orphan word under a
     full line. */
  line-height:1.15;
  text-wrap:balance;
}
html[data-ui="v2"] .stat span{
  margin-top:6px;
  /* 11px uppercase with letter-spacing loses ascenders and descenders
     on top of being small; 12px is the floor for a legible label */
  font-size:12px;
  font-weight:600;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--c-text-secondary);
}

html[data-ui="v2"] .view-toggle{
  padding:3px;
  border-radius:var(--r-pill);
  background:var(--v2-surface-2);
}
html[data-ui="v2"] .view-toggle button{
  height:34px;
  border-radius:var(--r-pill);
  transition:background var(--v2-dur-1) var(--v2-ease),
             color var(--v2-dur-1) var(--v2-ease);
}
/* Selected segment. --c-white points at the dark surface (#141416) in
   dark mode, which is darker than the track it sits on (#1C1C1F): the
   selected segment read as the unselected one. --v2-raised is defined
   per mode for exactly this. The fill alone stays below 3:1 in dark —
   a segmented control cannot carry a 3:1 fill without turning into a
   row of buttons — so the state is also carried by the label, which
   goes from --c-text-secondary to --c-text-primary (6.7:1 apart), and
   by aria-pressed. */
html[data-ui="v2"] .view-toggle button{ color:var(--c-text-secondary); }
html[data-ui="v2"] .view-toggle button.is-on{
  background:var(--v2-raised);
  color:var(--c-text-primary);
  font-weight:600;
  box-shadow:inset 0 0 0 1px var(--v2-raised-ring), var(--shadow-nav);
}

/* the rank as a large numeral rather than a pill: it is the piece of
   information this screen has to give first */
/* Right-aligned, not centred: a "1" and a "10" centred each in their
   own box do not line up from one row to the next. Direct child only,
   so the pill version on the photo (.thumb__rank) stays centred. */
html[data-ui="v2"] .rank-row > .rank-pos{ justify-content:flex-end; }
html[data-ui="v2"] .rank-pos{
  width:auto;
  min-width:38px;
  height:auto;
  border-radius:0;
  background:transparent;
  color:var(--c-text-secondary);
  font-size:24px;
  font-weight:800;
  letter-spacing:-0.04em;
  font-variant-numeric:tabular-nums;
}
html[data-ui="v2"] .rank-pos--1,
html[data-ui="v2"] .rank-pos--2,
html[data-ui="v2"] .rank-pos--3{
  background:transparent;
  color:var(--c-text-primary);
}
html[data-ui="v2"] .rank-pos--1{ color:var(--v2-gold); }

/* sur la photo, le rang redevient une pastille : il lui faut un fond
   pour rester lisible sur n'importe quelle image */
html[data-ui="v2"] .thumb__rank{
  top:12px; left:12px;
  width:34px; height:34px;
  min-width:0;
  border-radius:var(--r-pill);
  background:var(--v2-glass-strong);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  backdrop-filter:blur(10px) saturate(160%);
  color:var(--c-text-primary);
  font-size:var(--fs-small);
  font-weight:700;
  box-shadow:0 2px 10px rgba(0,0,0,.18);
}
/* --v2-gold (#B4881F) only holds 2.72:1 against the frosted pill, and
   this label is 14px bold, so normal text: the darker gold of the
   badges is the one that passes. In dark mode both tokens resolve to
   the same light gold. */
html[data-ui="v2"] .thumb__rank.rank-pos--1{ color:var(--c-gold-text); }

html[data-ui="v2"] .rank-likes{
  color:var(--c-text-primary);
  font-weight:600;
  font-variant-numeric:tabular-nums;
}
html[data-ui="v2"] .rank-likes svg{ color:var(--c-red); }

/* the leading card is no longer ringed in gold: the card has no chrome
   left, so the accent sits on the thumbnail */
html[data-ui="v2"] .card.is-first{ box-shadow:none; }
html[data-ui="v2"] .card.is-first:hover,
html[data-ui="v2"] .card.is-first:focus-within{ box-shadow:none; }
html[data-ui="v2"] .card.is-first .thumb{ box-shadow:0 0 0 2px var(--v2-gold); }
html[data-ui="v2"] .card.is-first:hover .thumb,
html[data-ui="v2"] .card.is-first:focus-within .thumb{
  box-shadow:0 0 0 2px var(--v2-gold), var(--shadow-hover);
}

html[data-ui="v2"] .rank-row{
  gap:var(--sp-4);
  padding:var(--sp-3) var(--sp-5);
  border-bottom-color:var(--v2-hairline);
  transition:background var(--v2-dur-1) var(--v2-ease);
}
/* the V1 pins this container at flex:0 0 auto, so it keeps its
   max-content width: a photo in a dozen categories pushed the like
   counter clean out of the row */
html[data-ui="v2"] .rank-row .cat-tags{
  flex:0 1 auto;
  flex-wrap:nowrap;
  overflow:hidden;
  min-width:0;
}
html[data-ui="v2"] .rank-row:hover{ background:var(--v2-surface-2); }
html[data-ui="v2"] .rank-row.is-first,
html[data-ui="v2"] .rank-row.is-first:hover{ background:var(--c-gold-soft); }
/* the gold badge and the leading row share the same hue: on that row a
   solid pill would disappear, so it becomes an outline */
html[data-ui="v2"] .badge--gold{
  background:transparent;
  box-shadow:inset 0 0 0 1px currentColor;
  color:var(--c-gold-text);
}
html[data-ui="v2"] .thumb__badge.badge--gold{
  background:var(--v2-glass-strong);
  box-shadow:0 2px 10px rgba(0,0,0,.18);
  color:var(--c-gold-text);
}
html[data-ui="v2"] .rank-main b{ font-weight:600; letter-spacing:-0.015em; }
html[data-ui="v2"] .mini-thumb{
  width:72px; height:54px;
  border-radius:var(--r-btn);
}

/* ---------------------------------------------------------
   12. Administration
   --------------------------------------------------------- */
/* underlined tabs rather than pills: quieter on an already dense
   screen, and the active line reads from a distance */
/* The rule under the tabs is drawn as an inset shadow rather than a
   border, and the active tab no longer needs a -1px margin to sit on
   top of it. .tabs inherits overflow-x:auto from the V1, and CSS
   forbids overflow-y:visible next to it: a child hanging 1px below the
   content box forced a vertical scrollbar, or got clipped. */
html[data-ui="v2"] .tabs{
  gap:var(--sp-5);
  padding:0;
  margin-bottom:var(--sp-6);
  border-radius:0;
  background:transparent;
  border-bottom:0;
  box-shadow:inset 0 -1px 0 var(--v2-hairline);
}
html[data-ui="v2"] .tab{
  height:44px;
  padding:0;
  border-radius:0;
  border-bottom:2px solid transparent;
  font-weight:500;
}
html[data-ui="v2"] .tab.is-on{
  background:transparent;
  box-shadow:none;
  border-bottom-color:var(--c-text-primary);
  font-weight:600;
}
html[data-ui="v2"] .tab__count{
  background:var(--c-red-soft);
  color:var(--c-red-text);
  font-variant-numeric:tabular-nums;
}

html[data-ui="v2"] .data-row{ padding:var(--sp-3) var(--sp-5); border-bottom-color:var(--v2-hairline); }
html[data-ui="v2"] .data-row--head{
  background:transparent;
  border-bottom-color:var(--v2-hairline);
  font-size:12px;
  letter-spacing:.06em;
}
html[data-ui="v2"] .data-row:not(.data-row--head):hover{ background:var(--v2-surface-2); }

html[data-ui="v2"] .setting-line{ padding:var(--sp-5); border-bottom-color:var(--v2-hairline); }
/* with no ceiling, a slightly long label takes the whole width and
   pushes its control to the next line: the settings then stop lining
   up under one another */
html[data-ui="v2"] .setting-line__label{ max-width:72ch; }
html[data-ui="v2"] .setting-line__label b{ font-weight:600; letter-spacing:-0.015em; }
/* the hex field is calibrated to the pixel in V1: V2's 15px of padding
   was truncating the #RRGGBB inside it */
html[data-ui="v2"] .setting-line__value .input.input--hex{ width:112px; }
/* The "add an administrator" field carries a 220px width set inline,
   calibrated for V1's 15px; at 16px (the size iOS's automatic zoom
   forces on us) its "prenom.nom@icade.fr" template is truncated by a
   few pixels. min-width beats the inline width without having to reach
   for an !important. */
/* The 46px of the V2 field (0,2,1) beat the 38px the V1 sets for a
   settings row (layout.css, (0,2,0)): on the same line a 46px input
   sat next to a 38px value chip and a 38px color swatch. */
html[data-ui="v2"] .setting-line__value .input,
html[data-ui="v2"] .setting-line__value .select{ height:38px; }
/* same story as the hex field two rules up: JJ/MM/AAAA plus the calendar
   indicator does not fit the V1 width once the field is at 16px, the size
   iOS's automatic zoom forces on anything typed into */
html[data-ui="v2"] .setting-line__value .input[type="date"]{ min-width:176px; }
/* the three fields of « Ajouter quelqu'un au concours », same reason: 16px
   text needs more room than the inline widths the V1 sets */
html[data-ui="v2"] #add-participant-email{ min-width:248px; }
html[data-ui="v2"] #add-participant-name{ min-width:180px; }
html[data-ui="v2"] #add-participant-role{ min-width:168px; }
/* same story on the reminder delay: 150px inline, and « 3 jours avant »
   runs into the chevron at 16px */
html[data-ui="v2"] #voting-reminder-days{ min-width:172px; }
/* The V1 sets a handful of gutters off the --sp-* scale (18px, 9px,
   10px). They were never remapped, so they stayed put in compact
   density while everything around them tightened — the only blocks in
   the app that did not answer the setting. */
html[data-ui="v2"] .mod-item{
  padding:var(--sp-5);
  gap:var(--sp-5);
  border-bottom-color:var(--v2-hairline);
}
html[data-ui="v2"] .mod-item__body{ gap:var(--sp-2); }
html[data-ui="v2"] .mod-item__tags,
html[data-ui="v2"] .mod-item__actions{ gap:var(--sp-2); }
html[data-ui="v2"] .setting-line__value,
html[data-ui="v2"] .setting-line__extra{ gap:var(--sp-3); }
/* .mod-item is a <div> in the moderation queue but a <button> in "Mes
   photos", where the whole row opens the lightbox. A button sizes
   itself to its content even as a flex container, so those rows were
   as wide as their longest line — 381px, 722px and 1098px inside the
   same 1100px section — and each row's bottom rule stopped wherever
   its text did. */
html[data-ui="v2"] button.mod-item{ width:100%; }
html[data-ui="v2"] .mod-item__thumb{ border-radius:var(--v2-r-media); }

/* ---------------------------------------------------------
   13. Modals, lightbox, file drop zone, toasts
   --------------------------------------------------------- */
html[data-ui="v2"] .modal-backdrop{
  background:rgba(12,10,9,.42);
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
}
html[data-ui="v2"] .modal{
  border-radius:var(--v2-r-modal);
  border:1px solid var(--v2-hairline);
  animation:v2-modal-in 180ms var(--v2-ease) both;
}
@keyframes v2-modal-in{
  from{ opacity:0; transform:scale(.97) translateY(6px); }
  to{ opacity:1; transform:none; }
}
html[data-ui="v2"] .modal__head{ padding:var(--sp-5) var(--sp-6); border-bottom-color:var(--v2-hairline); }
html[data-ui="v2"] .modal__head h3{ letter-spacing:-0.02em; }
html[data-ui="v2"] .modal__foot{ border-top-color:var(--v2-hairline); }
html[data-ui="v2"] .modal__close{ border-radius:var(--r-pill); }
html[data-ui="v2"] .modal__close:hover{ background:var(--v2-surface-2); }

/* The dashed border is what says "drop here": taken down to 1px it
   read as a plain hairline and the zone looked like an inert card.
   1.5px, plus a very slightly tinted background to lift it off the
   modal's white. */
html[data-ui="v2"] .drop{
  padding:var(--sp-8) var(--sp-5);
  border:1.5px dashed var(--c-grey-border);
  background:var(--v2-surface-2);
  transition:background var(--v2-dur-1) var(--v2-ease),
             border-color var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .drop:hover,
html[data-ui="v2"] .drop.is-over{
  border-color:var(--c-primary);
  background:var(--c-primary-light);
}
html[data-ui="v2"] .preview{ border-radius:var(--v2-r-media); }
html[data-ui="v2"] .preview__change{
  background:var(--v2-glass-strong);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  backdrop-filter:blur(10px) saturate(160%);
  color:var(--c-text-primary);
}

/* la lightbox reste sombre dans les deux modes : c'est une salle de
   projection, le fond neutre profond met la photo en avant */
html[data-ui="v2"] .modal-backdrop--lightbox{ background:rgba(9,9,11,.94); }
html[data-ui="v2"] .lightbox{
  border-radius:var(--v2-r-modal);
  border:0;
  background:#111113;
}
/* background-color, not the shorthand: .lightbox__stage carries the
   same 20px blurred placeholder as the cards, spread by a
   background-size:cover (components.css) that the shorthand was
   resetting — the bands left by a photo that does not fill the stage
   then showed the placeholder tiled at its native size. */
html[data-ui="v2"] .lightbox__stage{
  background-color:#111113;
  background-size:cover;
  background-position:center;
  background-repeat:no-repeat;
}
/* veil over the stage's blurred placeholder: purple in V1, neutral
   here like the rest of the screening room. Do not touch
   .lightbox__img's z-index, which is what lifts the photo above this
   veil. */
html[data-ui="v2"] .lightbox__stage::before{ background:rgba(9,9,11,.72); }
html[data-ui="v2"] .lightbox__stage.is-loading::before{ background:rgba(9,9,11,.5); }
/* .lightbox__img caps itself at 70vh (50vh on mobile) whatever the
   panel below needs; a 300-character rejection reason, or simply a
   phone in landscape, pushed the stage past that cap and the image was
   cut off by .lightbox{overflow:hidden}. The image now takes the room
   the stage actually has, and a long panel scrolls instead of growing. */
html[data-ui="v2"] .lightbox__img{ max-height:100%; }
html[data-ui="v2"] .lightbox__info{
  background:#111113;
  border-top:1px solid rgba(255,255,255,.08);
  max-height:52%;
  overflow-y:auto;
  overscroll-behavior:contain;
}
html[data-ui="v2"] .lightbox__close,
html[data-ui="v2"] .lightbox__nav{
  background:rgba(255,255,255,.12);
  -webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);
  transition:background var(--v2-dur-1) var(--v2-ease);
}
html[data-ui="v2"] .lightbox__close:hover,
html[data-ui="v2"] .lightbox__nav:hover{ background:rgba(255,255,255,.24); }
html[data-ui="v2"] .lightbox__caption{ letter-spacing:-0.02em; }
/* The voting rule used to sit between the heart and the download
   button, at the same size as their labels: a sentence wedged between
   two controls reads as a third one and pushes them apart. It belongs
   under them, as the caption it is. */
html[data-ui="v2"] .lightbox__quota{
  order:3;
  flex:1 0 100%;
  margin-top:2px;
}

html[data-ui="v2"] .toast{
  border-radius:var(--r-pill);
  padding:12px 20px;
  font-weight:500;
  background:var(--v2-ink);
  color:var(--v2-on-ink);
}
/* The ink cannot be a hardcoded white: in dark mode the semantic
   colors flip to light tints (#4ADE80, #FB7185) and white on them fell
   to 1.74:1 and 2.69:1. --v2-on-ink is white in light mode and near
   black in dark, which is the same rule the accent buttons follow. */
html[data-ui="v2"] .toast--ok{ background:var(--c-green-dark); color:var(--v2-on-ink); }
html[data-ui="v2"] .toast--err{ background:var(--c-red); color:var(--v2-on-ink); }

/* ---------------------------------------------------------
   14. Connexion
   --------------------------------------------------------- */
html[data-ui="v2"] .auth__visual::after{ background:rgba(12,10,9,.62); }
html[data-ui="v2"] .auth__visual h2{
  font-size:clamp(28px, 3vw, 42px);
  letter-spacing:-0.03em;
  line-height:1.06;
}
html[data-ui="v2"] .auth__kicker{
  background:rgba(255,255,255,.18);
  -webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);
  letter-spacing:.06em;
}
html[data-ui="v2"] .auth__box{ max-width:400px; }
html[data-ui="v2"] .auth__lead{ font-size:var(--fs-body); }

/* ---------------------------------------------------------
   13 bis. Pending and failure states

   Added by 76e85c5 and 856a543 (visual feedback on every action, an
   explicit reason when an action is impossible). Most of that markup
   already consumes the tokens and therefore follows the remap; only
   the places where a color is hardcoded, and those where V2 has to
   adjust the metrics, are picked up here.
   --------------------------------------------------------- */

/* Spinners. On a solid button the spinner's ink must be the label's:
   white in light mode, dark in dark mode, where the accent button
   carries dark text. On the light variants, V1 hardcodes an
   rgba(25,3,64,.2) that is invisible on a dark surface. */
html[data-ui="v2"] .btn--loading::after{
  border-color:var(--v2-spin-track);
  border-top-color:var(--v2-spin-ink);
}
html[data-ui="v2"] .btn--light.btn--loading::after,
html[data-ui="v2"] .btn--grey.btn--loading::after,
html[data-ui="v2"] .btn--outline.btn--loading::after,
html[data-ui="v2"] .btn--ghost.btn--loading::after,
html[data-ui="v2"] .btn--secondary.btn--loading::after,
html[data-ui="v2"] .dropdown__item.btn--loading::after{
  border-color:var(--c-grey-border);
  border-top-color:var(--c-primary);
}
/* la pastille de couleur reste un aplat de la couleur de marque dans
   les deux modes : son spinner reste blanc */
html[data-ui="v2"] .color-preset.is-saving::after{
  border-color:rgba(255,255,255,.45);
  border-top-color:#FFFFFF;
}

/* Skeletons. V2 rounds .sk-bar to 6px, which would override the
   "button" shape V1 defines after it. */
html[data-ui="v2"] .sk-bar--button{ height:46px; border-radius:var(--r-btn); }

/* Failed load, in place of the skeleton */
html[data-ui="v2"] .load-error{ padding:var(--sp-8) var(--sp-5); }
html[data-ui="v2"] .load-error__msg{ font-size:var(--fs-body); max-width:52ch; }

/* Settings currently being saved */
html[data-ui="v2"] .setting-saving{ font-variant-numeric:tabular-nums; }
html[data-ui="v2"] .switch--busy .switch__track{ opacity:.5; }

/* Photo submission. .preview__name hardcodes a white background under
   text that follows --c-text-primary: in dark mode that was white on
   white. Same fix as .preview__change. */
html[data-ui="v2"] .preview__name{
  background:var(--v2-glass-strong);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  backdrop-filter:blur(10px) saturate(160%);
  color:var(--c-text-primary);
  border-radius:var(--r-pill);
}
html[data-ui="v2"] .preview--pending .preview__placeholder{
  background:var(--v2-surface-2);
}
html[data-ui="v2"] .upload-progress__track{
  height:4px;
  background:var(--v2-surface-2);
  box-shadow:inset 0 0 0 1px var(--v2-hairline);
}
html[data-ui="v2"] .upload-progress__fill{
  transition:width var(--v2-dur-2) var(--v2-ease);
}
html[data-ui="v2"] .upload-progress__label{ font-variant-numeric:tabular-nums; }

/* Contest closed: the title bar's primary button becomes a badge,
   which has to keep the same presence as the button it replaces */
html[data-ui="v2"] .page-head--split .badge{
  height:38px;
  padding:0 16px;
  font-size:var(--fs-small);
  font-weight:500;
}

/* ---------------------------------------------------------
   14 bis. Surfaces that stay dark in both modes

   The dark-mode remap points --c-white at the dark surface: that is
   what gives an accent button its dark, AA-compliant text. But
   everywhere --c-white serves as ink on a photo or on the lightbox
   panel — which stay dark themselves — the color has to remain a real
   white. These rules are written unconditionally: in light mode
   --c-white is already #FFFFFF, so they change nothing there.
   --------------------------------------------------------- */
html[data-ui="v2"] .winner__content,
html[data-ui="v2"] .winner__pill,
html[data-ui="v2"] .auth__visual-inner,
html[data-ui="v2"] .auth__visual-inner .brand,
html[data-ui="v2"] .auth__kicker,
html[data-ui="v2"] .lightbox__close,
html[data-ui="v2"] .lightbox__nav,
html[data-ui="v2"] .lightbox__info,
html[data-ui="v2"] .lightbox__info .reason-box b{ color:#FFFFFF; }
html[data-ui="v2"] .lightbox__spin{ border-top-color:#FFFFFF; }
html[data-ui="v2"] body.using-keyboard .lightbox__close:focus,
html[data-ui="v2"] body.using-keyboard .lightbox__nav:focus{ outline-color:#FFFFFF; }

/* the pills on the lightbox panel keep a light fill (a V1 rule): their
   text must therefore stay dark, even when the rest of the interface is
   in dark mode */
/* 4.27:1 with the #15803D of the other green badges: this pastel is
   lighter than the one on a white surface, so the ink has to be too */
html[data-ui="v2"] .lightbox__info .badge--green{ background:#DCF3DC; color:#146B34; }
html[data-ui="v2"] .lightbox__info .badge--orange{ background:#FBEBD2; color:#9A4B08; }
html[data-ui="v2"] .lightbox__info .badge--red{ background:#FADBDF; color:#BE123C; }
html[data-ui="v2"] .lightbox__info .badge--gold{
  background:#F6EBD4;
  color:#8A6314;
  box-shadow:none;
}
html[data-ui="v2"] .lightbox__info .cat-tag{
  background:transparent;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.42);
  color:rgba(255,255,255,.82);
}

/* the like and the download button would otherwise inherit light-mode
   text colors, unreadable on the dark panel */
html[data-ui="v2"] .lightbox__info .like-btn{ color:rgba(255,255,255,.86); }
html[data-ui="v2"] .lightbox__info .like-btn:hover{
  background:rgba(255,255,255,.12);
  color:#FFFFFF;
}
html[data-ui="v2"] .lightbox__info .like-btn.is-liked,
html[data-ui="v2"] .lightbox__info .like-btn--static.is-liked:hover{
  background:rgba(251,113,133,.20);
  color:#FDA4AF;
}
html[data-ui="v2"] .lightbox__info .btn--light{
  background:rgba(255,255,255,.14);
  color:#FFFFFF;
  /* the neutral hairline set on .btn--light above is calibrated for a
     light surface: on this panel it cannot be seen and only thickens
     the button */
  box-shadow:none;
}
html[data-ui="v2"] .lightbox__info .btn--light:hover{ background:rgba(255,255,255,.24); }
/* the panel stays dark: the light variants' spinner, keyed on
   --c-grey-border, would be lost on it */
html[data-ui="v2"] .lightbox__info .btn--loading::after{
  border-color:rgba(255,255,255,.3);
  border-top-color:#FFFFFF;
}

/* ---------------------------------------------------------
   15. Keyboard focus
   Anything the script has to distinguish from a mouse click goes
   through a home-grown flag, body.using-keyboard (see web/js/app.js,
   styles in base.css); the controls the user agent already gets right
   keep :focus-visible and only have their colour corrected. V2 reuses
   the same selectors with its own colors, base.css's white ring being
   invisible on a light background.
   --------------------------------------------------------- */
/* on .card__open, for the same reason as base.css: :focus-within also
   fired for the like sitting in .card__foot, so tabbing to the like drew
   a black box around the whole card on top of the button's own ring.
   Nothing clips it here — the V2 card is overflow:visible — so the ring
   stays outside, on the media radius. */
html[data-ui="v2"] body.using-keyboard .card__open:focus{
  outline:2px solid var(--c-text-primary);
  outline-offset:3px;
  border-radius:var(--v2-r-media);
}
/* the V2 like is red, its ring follows (base.css puts the V1 green here) */
html[data-ui="v2"] .like-btn:focus-visible{ outline-color:var(--c-red); }
/* A white ring alone disappears on a light photo (1:1 in the worst
   case). Two concentric rings, light over dark, are readable on any
   image; .winner clips its content, so they are drawn inside. */
html[data-ui="v2"] body.using-keyboard .winner:focus{
  outline:none;
  box-shadow:inset 0 0 0 3px #FFFFFF, inset 0 0 0 6px rgba(12,10,9,.75);
}
html[data-ui="v2"] body.using-keyboard .rank-row:focus{
  outline:2px solid var(--c-text-primary);
  outline-offset:-2px;
}
html[data-ui="v2"] body.using-keyboard .mod-item__thumb:focus{
  outline:2px solid var(--c-text-primary);
  outline-offset:3px;
}
/* the drop zone became focusable (role="button" + tabindex): it needs
   the same ring as the rest, the UA default being invisible on the
   dashed border */
html[data-ui="v2"] .drop:focus{ outline:none; }
html[data-ui="v2"] body.using-keyboard .drop:focus{
  outline:2px solid var(--c-text-primary);
  outline-offset:2px;
  border-color:var(--c-primary);
}

/* ---------------------------------------------------------
   16. Media queries
   Same thresholds as layout.css: 992 / max-991 / max-767.
   --------------------------------------------------------- */
@media (min-width:992px){
  html[data-ui="v2"] .winner-row--multi{
    grid-template-columns:repeat(auto-fit, minmax(340px, 1fr));
  }
}

@media (max-width:991px){
  html[data-ui="v2"] .navbar .nav-links{
    background:var(--v2-glass-strong);
    -webkit-backdrop-filter:blur(20px) saturate(180%);
    backdrop-filter:blur(20px) saturate(180%);
    border-bottom:1px solid var(--v2-hairline);
    /* the folded menu spans edge to edge (left:0;right:0): in landscape
       its first and last items sit under the notch */
    padding-left:max(10px, env(safe-area-inset-left));
    padding-right:max(10px, env(safe-area-inset-right));
  }
  html[data-ui="v2"] .burger:hover{ background:var(--v2-surface-2); }

  /* The V1 folds the participants table only below 768px, but the five
     tracks are already too narrow at 768: the photo/like counts overflow
     their cell. The fold starts where the table actually stops fitting.
     Same folded card as the V1's (cf layout.css): identity and actions
     face each other on the first line, counts and dates read as one
     sentence underneath. */
  html[data-ui="v2"] .data-row{
    grid-template-columns:minmax(0,1fr) auto;
    gap:var(--sp-2) var(--sp-3);
  }
  html[data-ui="v2"] .data-cell--who{ grid-column:1; }
  html[data-ui="v2"] .data-cell--role{
    grid-column:2;
    grid-row:1;
    align-self:start;
    flex-wrap:nowrap;
  }
  html[data-ui="v2"] .data-meta{
    grid-column:1 / -1;
    display:flex;
    flex-wrap:wrap;
    gap:2px var(--sp-1);
  }
  html[data-ui="v2"] .data-meta .data-cell{
    display:flex;
    align-items:baseline;
    gap:var(--sp-1);
    font-size:var(--fs-xsmall);
    color:var(--c-text-secondary);
  }
  html[data-ui="v2"] .data-meta .data-cell--num b{ font-size:inherit; }
  html[data-ui="v2"] .data-meta .data-cell--num b.is-zero{
    font-weight:600;
    color:var(--c-text-secondary);
  }
  html[data-ui="v2"] .data-meta > .data-cell:not(:last-child)::after{
    content:'·';
    /* the separator takes the colour of the text it separates, faded:
       a fixed grey is either invisible in dark mode or heavier than the
       words in light mode */
    opacity:.5;
  }
  html[data-ui="v2"] .data-cell--role .badge--grey{ display:none; }
  html[data-ui="v2"] .data-row--head{ display:none; }
  /* the sort bar replaces the header on the breakpoint where the header
     actually leaves — 991 here, not the V1's 767, or sorting would be
     unreachable between the two */
  html[data-ui="v2"] .sort-bar{
    display:flex;
    align-items:center;
    gap:var(--sp-2);
    flex:1 1 100%;
  }
  /* the V2 fields are 46px, not the 44 of the base layer: the button
     follows the select it sits against, or the two ends of the bar are
     off by two pixels */
  html[data-ui="v2"] .sort-bar__dir{ width:46px; height:46px; }
  /* the counts wrap inside their own cell rather than push the sentence
     wider than the card */
  html[data-ui="v2"] .data-cell--num{ flex-wrap:wrap; }
}

@media (max-width:767px){
  html[data-ui="v2"] .app-main{ padding-top:calc(var(--nav-h) + var(--demo-h) + 24px); }

  /* the screen's primary action takes the full width rather than
     floating on the left under the title, once the header has
     wrapped */
  html[data-ui="v2"] .page-head--split > .btn--primary{ width:100%; }
  /* Once the contest is closed the button is replaced by a badge
     carrying a full sentence, and .badge is white-space:nowrap: at
     375px it asked for ~340px of the 343 available, and overflowed
     below that. */
  html[data-ui="v2"] .page-head--split > .badge{
    width:100%;
    height:auto;
    min-height:38px;
    padding:9px 16px;
    white-space:normal;
    text-align:center;
  }
  html[data-ui="v2"] .grid{ gap:var(--sp-6); }
  /* Portrait is the format of the reveal: it fills the screen and shows
     nothing but the winning photo. On a tie there are two of them,
     sometimes three — stacked at 4/5 they forced three screens of
     scrolling before the first photo of the feed. The square brings
     each back to half that height without cramping the layout of the
     overlaid content. */
  html[data-ui="v2"] .winner{ aspect-ratio:16 / 9; }
  html[data-ui="v2"] .winner-row--multi .winner{ aspect-ratio:4 / 3; }
  html[data-ui="v2"] .winner__content{ padding:var(--sp-5); }

  /* même arbitrage pour le podium, qui est le même hero en plus haut */
  html[data-ui="v2"] .podium-card{ aspect-ratio:4 / 5; }
  html[data-ui="v2"] .podium-top--multi .podium-card{ aspect-ratio:4 / 3; }
  html[data-ui="v2"] .podium-card__content{ padding:var(--sp-5); }

  /* the filters become a scrolling rail rather than several rows: the
     bar keeps a fixed height whatever the number of categories */
  html[data-ui="v2"] .feed-filters{
    flex-wrap:nowrap;
    overflow-x:auto;
    scroll-snap-type:x proximity;
    -ms-overflow-style:none;
    scrollbar-width:none;
    margin:0 calc(var(--sp-4) * -1);
    padding:0 var(--sp-4);
    /* the rail bleeds past the page padding and puts it back as its
       own padding; without this, snapping the first chip to "start"
       scrolls that padding away on load, and the rail alone starts
       flush against the screen edge while everything else on the
       screen keeps its margin */
    scroll-padding-left:var(--sp-4);
    scroll-padding-right:var(--sp-4);
    /* the rail has no scrollbar: without this fade, the last category is
       cut off sharp at the screen edge and nothing says there are more
       behind it */
    -webkit-mask-image:linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
    mask-image:linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
  }
  html[data-ui="v2"] .feed-filters::-webkit-scrollbar{ display:none; }
  html[data-ui="v2"] .chip{ scroll-snap-align:start; flex:0 0 auto; }

  html[data-ui="v2"] .tabs{ gap:var(--sp-4); }
  html[data-ui="v2"] .modal{ border-radius:0; border:0; }
  /* the V1 drops the radius on a full-screen sheet, but the V2 sets it
     with a higher specificity outside any media query */
  html[data-ui="v2"] .lightbox{ border-radius:0; }

  /* A ranking row keeps the desktop geometry here because the V2 sets
     gap and padding outside any media query, at a higher specificity
     than the V1 mobile rules: at 375px the row asked for 412px of the
     303 available, .rank-main collapsed to zero width and the like
     counter fell outside the row — neither the caption nor the count
     was on screen. The gold badge goes with the categories the V1
     already hides: the rank number and the tinted row already say the
     photo is in the lead. */
  html[data-ui="v2"] .rank-row{ gap:11px; padding:var(--sp-3) var(--sp-4); }
  html[data-ui="v2"] .rank-row .mini-thumb{ width:52px; height:44px; }
  html[data-ui="v2"] .rank-row .cat-tags,
  html[data-ui="v2"] .rank-row .badge--gold{ display:none; }
  html[data-ui="v2"] .rank-row > .rank-pos{ min-width:24px; font-size:19px; }
  html[data-ui="v2"] .rank-main{ min-width:0; }

  /* the rail bleeds to the edge of the viewport, which in landscape is
     under the notch */
  html[data-ui="v2"] .feed-filters{
    margin-left:calc(max(var(--sp-4), env(safe-area-inset-left)) * -1);
    margin-right:calc(max(var(--sp-4), env(safe-area-inset-right)) * -1);
    padding-left:max(var(--sp-4), env(safe-area-inset-left));
    padding-right:max(var(--sp-4), env(safe-area-inset-right));
    scroll-padding-left:max(var(--sp-4), env(safe-area-inset-left));
    scroll-padding-right:max(var(--sp-4), env(safe-area-inset-right));
  }
  /* a full-screen sheet has no page padding of its own to fall back on */
  html[data-ui="v2"] .modal__foot{
    padding-bottom:max(var(--sp-5), env(safe-area-inset-bottom));
    padding-left:max(var(--sp-5), env(safe-area-inset-left));
    padding-right:max(var(--sp-5), env(safe-area-inset-right));
  }
}

/* ---------------------------------------------------------
   17. Reduced motion
   --------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  html[data-ui="v2"] .winner,
  html[data-ui="v2"] .podium-card,
  html[data-ui="v2"] .reveal,
  html[data-ui="v2"] .modal{ animation:none; }
  html[data-ui="v2"] .winner__label::after,
  html[data-ui="v2"] .podium-card__label::after{ animation:none; opacity:0; }
  html[data-ui="v2"] .card__open:hover .thumb img{ transform:none; }
  html[data-ui="v2"] .btn:active{ transform:none; }
}
