/* ==========================================================================
   BRIDGE: the visual system → MudBlazor 9.2

   This file exists only because we render on MudBlazor. It disappears whole
   if the library ever changes — the dictionary of values lives in tokens.css
   and knows nothing about any of this.

   PRECEDENCE — read this before changing a dark colour anywhere.

   MudThemeProvider emits its palette under `:root` from a <style> in the
   body — that is, AFTER our <link> — so at equal specificity the provider
   would win. But html[data-theme="dark"] (0,1,1) beats :root (0,1,0)
   regardless of document order. That is not only a first-paint bootstrap:
   the selector keeps winning for as long as the attribute is on <html>, so
   for every variable listed in section 1 THIS FILE IS THE PERMANENT
   AUTHORITY and FlowtentTheme.PaletteDark is dead weight. PaletteDark is
   live only for the keys section 1 does not name.
   ThemeShellTests.Bridge_DarkPalette_AgreesWithPaletteDark keeps the two
   halves from drifting apart, because the losing half is silent.
   Light mode belongs entirely to FlowtentTheme; duplicating it here would
   lose that same race, this time against us.

   Measured, not assumed (2026-08-13, live DOM, /login): the provider emits
   one <style> in <body> whose 86 palette declarations open with `:root{`.
   Specificity 0,1,0 — so the plain html[data-theme="dark"] below is enough
   and no rule here needs !important. Re-run the check after a MudBlazor
   upgrade: if a later version emits under .mud-theme-provider or similar,
   raise the selector to html[data-theme="dark"]:root (0,2,0) rather than
   reaching for !important.

   Contents. Only section 1 is dark-only; everything else applies to both
   themes and reads its colour from tokens.css, which flips itself.

     1. dark palette             7. platform preview frame
     2. primary correction       8. post status scale
     3. alerts and snackbars     9. platform chips
     4. focus ring               10. layers
     5. motion                   11. empty and error states
     6. skeleton                 12. preflight chips
                                 13. page header
                                 14. navigation progress bar
                                 15. KPI tile
                                 16. Card
                                 17. Avatar
                                 18. List row
                                 19. Shell (P3)
                                 20. Page components (legacy app.css)
                                 21. Dashboard (P4/13)
                                 22. Auth (P5)
                                 23. Projects (P5)
                                 24. Kalendarz (P5)
   ========================================================================== */

/* ---- 1. Dark palette ----------------------------------------------------
   Permanent, not a bootstrap — see PRECEDENCE above. Every value mirrors
   FlowtentTheme.PaletteDark; the test fails the build if one drifts.       */

html[data-theme="dark"] {
  --mud-palette-primary: #B4A2FD;
  --mud-palette-primary-darken: #AA96FD;
  --mud-palette-primary-lighten: #261C4E;
  --mud-palette-primary-text: #1A1040;
  /* Not a Palette property — MudBlazor derives it from Primary at
     HoverOpacity, which is .0588. We keep .1: a 6% wash of the accent is
     invisible on #10161B, and this is the hover ground for every text and
     outlined button in dark mode. Deliberate divergence, not drift.       */
  --mud-palette-primary-hover: rgba(180, 162, 253, .1);
  --mud-palette-primary-rgb: 180, 162, 253;

  --mud-palette-secondary: #B4C0CA;
  --mud-palette-secondary-text: #10161B;
  --mud-palette-tertiary: #8B99A5;
  --mud-palette-tertiary-text: #10161B;

  --mud-palette-background: #10161B;
  --mud-palette-background-gray: #1E262E;
  --mud-palette-surface: #171E24;
  --mud-palette-appbar-background: #171E24;
  --mud-palette-appbar-text: #E9EEF3;
  --mud-palette-drawer-background: #171E24;
  --mud-palette-drawer-text: #E9EEF3;
  --mud-palette-drawer-icon: #B4C0CA;

  --mud-palette-text-primary: #E9EEF3;
  --mud-palette-text-primary-rgb: 233, 238, 243;
  --mud-palette-text-secondary: #B4C0CA;
  --mud-palette-text-disabled: #8B99A5;

  /* Each semantic needs four variables, not one. The fill alone leaves the
     provider's LIGHT -text, -darken and -rgb in place — measured on the
     live page under data-theme="dark": white on our light fills at
     1.83–2.39:1 for filled semantic buttons and chips (the app ships ten of
     the buttons and seven of the chips), and near-black ink at 1.67–2.12:1
     for outlined alerts, one of which sits on the anonymous /register page.

     -darken is the hover step. On a dark palette the fill is already the
     light value, so its hover goes LIGHTER — the same inversion the primary
     makes in section 2. Left to itself MudBlazor darkens the fill, which on
     a dark ground moves the wrong way.                                    */
  --mud-palette-success: #6FD39A;
  --mud-palette-success-text: #10161B;
  --mud-palette-success-darken: #80D8A6;
  --mud-palette-success-rgb: 111, 211, 154;
  --mud-palette-warning: #E3B34C;
  --mud-palette-warning-text: #10161B;
  --mud-palette-warning-darken: #E6BC62;
  --mud-palette-warning-rgb: 227, 179, 76;
  --mud-palette-error: #F08C86;
  --mud-palette-error-text: #10161B;
  --mud-palette-error-darken: #F29A94;
  --mud-palette-error-rgb: 240, 140, 134;
  --mud-palette-info: #6CB4EC;
  --mud-palette-info-text: #10161B;
  --mud-palette-info-darken: #7EBDEE;
  --mud-palette-info-rgb: 108, 180, 236;

  --mud-palette-lines-default: #2B3540;
  --mud-palette-lines-inputs: #2B3540;
  --mud-palette-table-lines: #2B3540;
  /* These two must never hold the same value, and for a while they did.
     Seven grids set Striped and Hoverable together, so an equal pair leaves
     every odd row with no hover feedback at all — on the app's primary work
     surface. FlowtentTheme.PaletteDark had the distinct pair and a comment
     saying so; this file outranked it and the fix never reached a pixel.  */
  --mud-palette-table-striped: #1E262E;
  --mud-palette-table-hover: #232C33;
  --mud-palette-divider: #2B3540;
  --mud-palette-divider-light: #232C33;
  --mud-palette-divider-rgb: 43, 53, 64;

  --mud-palette-action-default: #B4C0CA;
  /* Also not a Palette property. The provider derives it from ActionDefault;
     we derive from --ft-text-1 instead, so the wash is neutral rather than
     faintly blue. Deliberate divergence.                                  */
  --mud-palette-action-default-hover: rgba(233, 238, 243, .06);
  --mud-palette-action-disabled: #5D6B77;
  --mud-palette-action-disabled-background: #1E262E;

  --mud-palette-gray-default: #8B99A5;
  --mud-palette-gray-light: #B4C0CA;
  /* gray-darker is read by exactly two rules in MudBlazor 9.2, both the
     default tooltip: its ground and its arrow. Paired with dark-text as ink
     it measured 3.33:1 on the live tooltip — under AA at 12px, on a
     component ui-ux.md requires around every icon-only button, and none of
     the ~40 call sites passes Color. A light ground inverts the tooltip the
     way dark themes conventionally do and measures 9.83:1. dark-text stays
     as it is: its other readers are alert variants this app never renders. */
  --mud-palette-gray-darker: #B4C0CA;
  --mud-palette-skeleton: #1E262E;

  --mud-palette-overlay-dark: rgba(4, 8, 12, .6);
  --mud-palette-dark: #E9EEF3;
  --mud-palette-dark-text: #10161B;
}

/* The document surface has to move with the palette. Without this the dark
   theme stops at the edge of the content and white glares around it.

   color-scheme is NOT set here — tokens.css owns it. It is a property of the
   design system, not of MudBlazor, and would have died with this file.     */
html[data-theme="dark"] body { background: var(--ft-bg); color: var(--ft-text-1); }

/* ---- 2. Primary correction — the one mapping MudTheme gets wrong --------
   Both themes, not just dark.

   MudBlazor paints a filled primary button with --mud-palette-primary and
   uses --mud-palette-primary-darken only for hover/focus/active. Our theme
   mirrors Primary onto --ft-accent (4.4:1) and PrimaryDarken onto
   --ft-accent-strong (5.5:1), so out of the box every primary CTA ships
   white text at 4.38:1 — under AA — and IMPROVES to 5.53:1 on hover, which
   is backwards. tokens.css forbids exactly this in its own comment.
   (Both numbers measured on the live login button before this block existed.)

   The fix cannot live in MudTheme: raising Primary to --ft-accent-strong
   would drag links, active icons and the focus ring up with it, and those
   want the lighter value. So the fill is corrected here and the hover goes
   darker rather than lighter.

   Text and outlined primary buttons have the same root cause in reverse:
   they use Primary as TEXT on a white surface, again 4.38:1 (4.09:1 on
   --ft-bg), while the dictionary already carries --ft-accent-text at 7.0:1
   for precisely that.

   Three components share the identical primary + primary-text construction,
   not one: the filled button, the FAB, and the filled chip. All three hover
   to primary-darken, so all three already carry the CORRECT fill on hover
   and the wrong one at rest. The Owner role chip renders 12px text at
   4.38:1 today.                                                            */

.mud-button-filled.mud-button-filled-primary,
.mud-fab-primary,
.mud-chip-filled.mud-chip-color-primary {
  background-color: var(--ft-accent-strong);
}
.mud-button-filled.mud-button-filled-primary:focus-visible,
.mud-button-filled.mud-button-filled-primary:active,
.mud-fab-primary:focus-visible {
  background-color: var(--ft-accent-text);
}

/* Hover is wrapped the way MudBlazor wraps its own. On a coarse pointer
   :hover latches after a tap and does not clear until you tap elsewhere, so
   an unwrapped rule leaves a tapped button sitting in its hover colour on
   every phone. The library guards against that; a bridge that overrides the
   library has to guard the same way or it reintroduces what was avoided.
   :focus-visible and :active stay outside the query — they are pointer-
   independent and have to keep working on a touch device.

   The chip selector carries .mud-clickable. MudBlazor's clickable-chip hover
   is (0,5,0) and the shorter (0,3,0) form loses to it — and PrimaryDarken is
   --ft-accent-strong, the same value the rest rule paints, so without the
   extension a clickable primary chip's hover equals its rest state.

   A chip WITHOUT .mud-clickable deliberately gets no hover. MudBlazor
   withholds it on purpose: a colour change under the cursor promises
   interactivity, and a decorative chip has none to offer.                  */
@media (hover: hover) and (pointer: fine) {
  .mud-button-filled.mud-button-filled-primary:hover,
  .mud-fab-primary:hover,
  .mud-chip-filled.mud-chip-color-primary.mud-clickable:hover:not(.mud-disabled) {
    background-color: var(--ft-accent-text);
  }
}
.mud-button-text.mud-button-text-primary,
.mud-button-outlined.mud-button-outlined-primary {
  color: var(--ft-accent-text);
}

/* ---- 3. Alerts and snackbars — MudTheme only got half of each semantic --
   tokens.css defines every semantic as a tint/deep PAIR and says the pair is
   the ground. MudTheme has one slot per semantic, so it received the deep
   half only, and MudBlazor derives what it needs from that single value:
   alert text by darkening ink that was already dark, and the alert ground by
   laying that same ink over the page at 6% alpha. The result is four
   near-black texts on four near-identical near-whites — measured #E9EFEE,
   #EFEDEA, #F2EBEC, #E8EEF3 — contrast fine, severity unreadable at a
   glance.

   Counted in the markup rather than guessed: 30 alerts, 23 of them on the
   default Variant.Text, 7 on Variant.Outlined, none on Variant.Filled. The
   23 include the fourteen "project not found" banners and both messages on
   the login screen, which are the only feedback an unauthenticated user
   ever gets; one of the 7 sits on /register, also anonymous.               */

.mud-alert-text-success  { background-color: var(--ft-success-tint); color: var(--ft-success-deep); }
.mud-alert-text-warning  { background-color: var(--ft-warning-tint); color: var(--ft-warning-deep); }
.mud-alert-text-error    { background-color: var(--ft-danger-tint);  color: var(--ft-danger-deep); }
.mud-alert-text-info     { background-color: var(--ft-info-tint);    color: var(--ft-info-deep); }

/* The icon keeps MudBlazor's own colour, --mud-palette-{severity}, at
   (0,2,0). That is intentional and not luck: Palette.Success and friends
   are mirrored from the same --ft-{severity}-deep the text above uses, so
   icon and text land on one value. Retune one and the other follows.      */

/* Outlined carries ink and a border, no ground, so both have to be named —
   MudBlazor takes the ink from {severity}-darken and the border from
   {severity}-rgb, and under a dark palette that is where the twelve
   variables in section 1 do their work. border-color, not the border
   shorthand: the width and style are MudBlazor's business.                */
.mud-alert-outlined-success { color: var(--ft-success-deep); border-color: var(--ft-success-deep); }
.mud-alert-outlined-warning { color: var(--ft-warning-deep); border-color: var(--ft-warning-deep); }
.mud-alert-outlined-error   { color: var(--ft-danger-deep);  border-color: var(--ft-danger-deep); }
.mud-alert-outlined-info    { color: var(--ft-info-deep);    border-color: var(--ft-info-deep); }

/* Filled is NOT dead code, and the count above is why it looks that way:
   no MudAlert in the app asks for it, but MudSnackbar renders its toasts
   through the very same classes. Verified on a live toast, not inferred
   from the assembly — saving a project name produces
   `class="mud-snackbar mud-alert-filled-success"` with a computed
   background of rgb(22,107,63), which is this rule painting --ft-success-
   deep. The app has 112 Snackbar.Add call sites and no SnackbarConfiguration,
   so these four rules are the styling of every toast it shows. Deleting
   them restyles all of them.                                              */
.mud-alert-filled-success { background-color: var(--ft-success-deep); }
.mud-alert-filled-warning { background-color: var(--ft-warning-deep); }
.mud-alert-filled-error   { background-color: var(--ft-danger-deep); }
.mud-alert-filled-info    { background-color: var(--ft-info-deep); }
.mud-alert-filled-success,
.mud-alert-filled-warning,
.mud-alert-filled-error,
.mud-alert-filled-info { color: var(--ft-surface); }

/* ---- 4. Focus ring — one pattern for the whole product ------------------
   ≥ 3:1 against its surroundings in both themes. Applies to cards that are
   clickable as a whole and to calendar cells, not just to native controls.
   :focus-visible rather than :focus — a mouse click should not leave a ring
   behind, which is why the browser default gets replaced rather than
   removed.

   `html :focus-visible`, not a bare `:focus-visible`. Measured: the bare
   form paints nothing at all on the two controls that matter most, because
   MudBlazor ships `button:focus{outline:none}` and `a:focus-visible
   {outline:none}` at (0,1,1) and a bare pseudo-class is (0,1,0). Adding the
   `html` type selector costs one c-column point, lands the same (0,1,1), and
   wins on document order — verified on a live button and link, which go from
   `3px none` to `2px solid #7C5CFC` with the 2px offset.

   Text inputs deliberately keep their own focus treatment: MudBlazor
   suppresses their outline at (0,3,1) and draws a primary underline instead,
   which is a visible indicator already. This rule does not try to outrank
   that; doing so would double the signal on every field in the app.

   No border-radius here, though the plan carried one. Measured with the
   declaration injected at this exact cascade position: a focused icon button
   and FAB dropped from 50% to 6px and a filled button from 10px to 6px —
   round controls squaring off the instant they take focus. The outline
   follows the element's own radius regardless, so it bought nothing.      */

html :focus-visible {
  outline: var(--ft-focus-ring-width) solid var(--ft-focus-ring);
  outline-offset: var(--ft-focus-ring-offset);
}

/* P3: FocusOnNavigate przenosi fokus na h1 PROGRAMOWO przy kazdej nawigacji,
   wiec bez bramki ring rysowal sie na tytule przy kazdym zimnym ladowaniu,
   u wszystkich uzytkownikow (zmierzone w P2: 18/18 zrzutow). Tlumimy go,
   dopoki uzytkownik nie nawiguje klawiatura (data-input ustawia
   scripts/input-modality.ts). Goly selektor h1, nie .ft-pagehead-title:
   FocusOnNavigate celuje w KAZDE h1, a strony auth/bledow (Login, Error,
   404...) renderuja h1 bez PageHeadera. h1 nigdy nie jest w tab-orderze,
   wiec poza nawigacja klawiaturowa jego ring nie niesie zadnej informacji. */
html:not([data-input="kbd"]) h1:focus-visible { outline: none; }

/* ---- 5. Motion ----------------------------------------------------------
   box-shadow is in the list on purpose. MudBlazor puts a 250ms box-shadow
   transition on .mud-button, .mud-fab and .mud-nav-link and 300ms on
   .mud-chip; this rule ties them at (0,1,0) and wins on order, so leaving
   the property out does not fall back to theirs — it removes it, and every
   elevated and filled button snaps its shadow instead of raising it.      */

.ft-hover-lift {
  transition: box-shadow var(--ft-dur-base) var(--ft-ease),
              transform var(--ft-dur-base) var(--ft-ease),
              background-color var(--ft-dur-base) var(--ft-ease);
}
.ft-hover-lift:hover {
  box-shadow: var(--ft-shadow-md);
  transform: translateY(var(--ft-lift));
  background-color: var(--ft-surface-2);
}

.mud-button-root, .mud-icon-button, .mud-nav-link, .mud-tab, .mud-chip {
  transition: background-color var(--ft-dur-fast) var(--ft-ease),
              color var(--ft-dur-fast) var(--ft-ease),
              border-color var(--ft-dur-fast) var(--ft-ease),
              box-shadow var(--ft-dur-base) var(--ft-ease);
}

@keyframes ft-shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }
@keyframes ft-fade-up { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---- 6. Skeleton --------------------------------------------------------
   Consumed by the Skeleton component in P2. The class lives here so the
   shimmer has one home by the time a component reaches for it.            */
.ft-shimmer {
  background: linear-gradient(90deg, var(--ft-skeleton-base) 25%, var(--ft-skeleton-sheen) 37%, var(--ft-skeleton-base) 63%);
  background-size: 800px 100%;
  animation: ft-shimmer 1.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  /* A flat plane, not a frozen gradient. Without this rule the global
     `animation-duration: 1ms !important` in tokens.css would stop the
     shimmer on an arbitrary frame, which reads as a broken element. The
     styleguide proposed an opacity pulse instead, but the same !important
     would kill that too; a flat placeholder carries the same "this is
     loading" meaning and needs no exception to the accessibility rule.    */
  .ft-shimmer { background: var(--ft-skeleton-base); animation: none; }
}

/* ---- 6a. Skeleton variants ----------------------------------------------
   Every variant carries the height of the content it stands in for. That is
   the whole point: a placeholder shorter than its content lets the page jump
   when data lands, which is the fault this component exists to remove.

   Heights come from the real thing — .ft-skel-row is 57px because that is
   ListRow's floor. .ft-skel-kpi is 122px, MEASURED live on Home.razor's
   dashboard grid (getBoundingClientRect, not added up from the rule sheet):
   16px top padding + 32px label (min-height, section 15) + 4px margin +
   52px display-scale value + 16px bottom padding + 1px border top and
   bottom. That decomposition happens to match the padding box on paper too,
   but the number in the rule below is the DOM measurement, so if either
   changes independently — a font metric, a border width MudBlazor didn't
   expect — this stays correct without anyone re-deriving it by hand.

   Measured on a KpiTile WITHOUT Context — none of Home.razor's six tiles
   pass it today. A tile with Context is taller by that line's own height and
   needs its own skeleton variant once a real call site renders one; this
   number does not cover it and should not be stretched to guess at it.      */
.ft-skel-row   { height: 57px; border-radius: var(--ft-r-md); }
.ft-skel-card  { height: 104px; border-radius: var(--ft-r-lg); }
.ft-skel-kpi   { height: 122px; border-radius: var(--ft-r-lg); }
.ft-skel-thumb { width: 40px; height: 40px; border-radius: var(--ft-r-sm); flex: 0 0 auto; }
.ft-skel-text  { height: 12px; border-radius: 4px; }
.ft-skel-text + .ft-skel-text { margin-top: 6px; }

/* .ft-skel-projcard (P5/T9) — ProjectCard.razor's own real height, added on
   paper from the §23 rule sheet (20px padding ×2 + 2px border, 42px monogram
   row, 12px+22px badges, 16px+20px stats, 14px+22px platforms, 16px+20px
   footer ≈ 226px for the common case WITH badges), not a live
   getBoundingClientRect() measurement like .ft-skel-kpi above —
   TODO-weryfikacja-T13: potwierdzić na realnym zrzucie ekranu listy
   projektów, bo karta bez badge'y jest krótsza o ok. 34px. */
.ft-skel-projcard { height: 226px; border-radius: var(--ft-r-lg); }

/* .ft-skel-block (P13/T2) — blok bez własnej wysokości: podaje ją parametr
   Height komponentu Skeleton (styl inline), bo szkielety stron szczegółów
   składają się z kilku bloków o różnych wysokościach i osobna klasa na każdą
   liczbę nic by nie wniosła. Promień jak karta. */
.ft-skel-block { border-radius: var(--ft-r-lg); }

/* ---- 7. Platform preview frame ------------------------------------------
   The inside of a preview keeps the platform's native background —
   Instagram is white in dark mode too. Unframed that reads as a rendering
   fault; with a border, a label and an overlay shadow it reads as a window
   into somewhere else, which is what it is.

   shadow-overlay is deliberate, not shadow-sm: this is a window ONTO another
   surface sitting inside the page, the same weight class as a dialog or a
   popover, not a card resting flush in the content flow. .ft-card below is
   the opposite end of the same shadow ramp on purpose — do not read the two
   as "the same move"; they solve different problems.

   Light-theme edge case, named and accepted: --ft-surface is white, same as
   an Instagram feed card's own background, so the frame's ground and the
   preview's native ground can coincide. Separation there rests on the
   hairline border and the preview's own internal chrome (avatar ring,
   caption padding), not on a colour contrast the frame cannot supply against
   an equally white platform surface. Dark mode has no such coincidence —
   --ft-surface is never white there.

   First consumer (P2/9): PlatformFrame.razor wraps PlatformPreview.razor's
   OUTER shell — the label + close button + dispatch that is our own chrome,
   not the platform's surface. It replaces that shell's ad-hoc MudPaper
   (Elevation=1, 10px radius, --ft-surface-2). Left as a known seam for P6:
   the right rail now shows two shadow languages side by side — this frame's
   overlay shadow next to the plain MudPaper Elevation=1 that SideMetaPanel
   and the other rail cards still use. Reconciling that is P6's rebuild, not
   this task's.

   Still open, still P6: the ~17 individual *Preview.razor components
   (InstagramFeedPostPreview.razor and siblings) keep their own scoped
   <style> blocks with real hex — Instagram white, its #DBDBDB border, its
   own radius — because that authenticity IS the point of a preview and is
   one of the app's ten documented markup-colour exceptions (spec §0).
   Folding those into this frame would mean rebuilding each one, not
   applying an existing class, so they are untouched here.                 */
.ft-preview-frame {
  border: 1px solid var(--ft-hairline);
  border-radius: var(--ft-r-lg);
  overflow: hidden;
  box-shadow: var(--ft-shadow-overlay);
  background: var(--ft-surface);
}
html[data-theme="dark"] .ft-preview-frame { border-color: var(--ft-decor); }

/* ---- 7a. Platform frame header/body --------------------------------------
   PlatformFrame's own layout. Kept off .ft-preview-frame itself, which still
   has to wrap platform-native content edge-to-edge whenever P6 gets to the
   inner previews — a header's padding has no business on that base class.  */
.ft-preview-frame-header {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px 0 12px;
}
.ft-preview-frame-label { font: var(--ft-type-micro); color: var(--ft-text-2); }
.ft-preview-frame-actions { margin-left: auto; }
/* Same gated-slot trap PageHeader already paid for in section 13: a caller
   that renders <Actions> with a false @if inside still emits the wrapper
   div, and flex's gap charges 8px for an empty one. */
.ft-preview-frame-actions:empty { display: none; }
.ft-preview-frame-body { padding: 8px 12px 12px 12px; }

/* ---- 8. Post status scale ----------------------------------------------
   Distinct from the semantic scale by SHAPE, not only colour: a status is a
   pill with a progress-wheel glyph, a semantic message is an r-md rectangle
   with ✓/!/✕/i. The two families stay apart even in greyscale, which is the
   point — colour alone is never the signal. StatusChip
   (Components/Shared/StatusChip.razor) consumes .ft-status and these
   modifiers directly. Calendar.razor's cell badge stays a MudChip — it
   renders the post topic, not the status label, so the pill's markup does
   not fit — but it now carries the colour via the modifier AND the glyph
   via StatusGlyph, so it is no longer a colour-only exception; only the
   pill's own geometry and label are what it does not adopt.

   The pair of selectors exists because Calendar.razor's cell badge still
   gets its modifier through an existing MudChip's Class — the pill's own
   markup does not fit there — so the .mud-chip-prefixed form is paired with
   each bare one: it has to outrank .mud-chip-filled.mud-chip-color-default
   at (0,2,0), and prefixing gets there without !important. The pairs are
   comma-joined so a token remap cannot reach one list and miss the other.

   LIMIT, and Task 8 checked it: this stops at (0,2,0), while
   .mud-chip-filled.mud-chip-color-default.mud-clickable:hover:not(.mud-disabled)
   is (0,5,0) — the same trap the primary chip sprang in section 2. Latent
   today because no status chip is clickable. The moment one is, its hover
   reverts to MudBlazor's default grey and this rule needs the clickable
   form too.

   Task 8 checked, and it bites: the calendar's status chips navigate to the
   post, so they carry .mud-clickable. Measured before the hover block below
   existed, .ft-status-progress went from #E9EAFC to #9AA7B4 on hover —
   MudBlazor's --mud-palette-action-disabled — leaving indigo text on grey at
   3.4:1 and no trace of which status the pill was reporting. The real rule
   is .mud-chip-filled.mud-clickable:hover:not(.mud-disabled) at (0,4,0), so
   the hover block clears it at (0,5,0).

   The hover HOLDS the pill's colours rather than darkening them. A darker
   ground would eat into the measured tint/deep contrast — published is the
   tightest at 5.2:1 and has the least to give — and the ramp has no designed
   hover step to darken toward. The feedback is a shadow instead, which costs
   the pair nothing. StatusChip itself carries no OnClick and no hover state
   — this shadow treatment stays scoped to the one MudChip caller that still
   needs it (Calendar.razor's clickable cell chip).                         */

/* Pill geometry for StatusChip (Components/Shared/StatusChip.razor), which
   puts "ft-status" on its own root span and adds a colour modifier from
   GetStatusChipClass beside it — padding, radius, font and the inline-flex
   layout all come from here now, not from .mud-chip-size-small.
   GetStatusChipClass on its own still returns the modifier alone:
   Calendar.razor's status chip is a MudChip that supplies its own geometry
   from .mud-chip-size-small and deliberately renders the post topic rather
   than the status label, so it takes the colour without this block.        */
.ft-status {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px 5px 9px;
  border-radius: var(--ft-r-full);
  font: var(--ft-type-micro);
  white-space: nowrap;
}
.ft-status svg { flex: 0 0 auto; }
/* ft-status-label — the span StatusChip.razor:14 points back at ("see the
   bridge, section 8"). It exists so a host's narrowest breakpoint can hide
   the text and keep the glyph (ListRow does, below 600px — section 18) — no
   styling of its own; a flex item's inline-level display blockifies
   regardless, so the `display: inline` this class used to carry was dead
   weight and is gone. When a host hides the label, it applies
   .ft-visually-hidden below (or, for a breakpoint-scoped hide, the same clip
   pattern duplicated locally — ListRow does this, section 18) rather than
   display:none — the label is the pill's only accessible name. */

/* Visually hidden, accessible to assistive tech. The clip pattern, not
   display:none — the point is to keep the text in the accessibility tree
   while removing the pixels. ListRow's narrowest breakpoint (Task 10) and
   the calendar badge's status name both rely on it. */
.ft-visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* StatusGlyph's own SVG, when it rides inside a MudChip instead of inside
   the .ft-status pill — Calendar.razor's cell badge is the one caller that
   needs the glyph without the rest of StatusChip (see the note above on
   Calendar staying a MudChip). .mud-chip-content is display:inline-flex
   with no gap of its own, so the glyph needs its own spacing here; .ft-status
   already supplies both via its own gap. What actually prevents a glyph from
   picking up both rules at once — this margin AND .ft-status's gap — is not
   the .mud-chip selector prefix: StatusGlyph's own svg would match it too,
   if it were ever nested inside a MudChip. It is that StatusChip is never
   rendered inside a MudChip anywhere in this codebase, so the two contexts
   never overlap on the same element. */
.mud-chip svg.ft-status-glyph { flex: 0 0 auto; margin-right: 4px; }

.ft-status-idea,
.mud-chip.ft-status-idea      { background: var(--ft-st-idea-tint);      color: var(--ft-st-idea-deep); }
.ft-status-planned,
.mud-chip.ft-status-planned   { background: var(--ft-st-planned-tint);   color: var(--ft-st-planned-deep); }
.ft-status-progress,
.mud-chip.ft-status-progress  { background: var(--ft-st-progress-tint);  color: var(--ft-st-progress-deep); }
.ft-status-pending,
.mud-chip.ft-status-pending   { background: var(--ft-st-pending-tint);   color: var(--ft-st-pending-deep); }
.ft-status-published,
.mud-chip.ft-status-published { background: var(--ft-st-published-tint); color: var(--ft-st-published-deep); }

.mud-chip.ft-status-idea.mud-clickable:hover:not(.mud-disabled)      { background: var(--ft-st-idea-tint); }
.mud-chip.ft-status-planned.mud-clickable:hover:not(.mud-disabled)   { background: var(--ft-st-planned-tint); }
.mud-chip.ft-status-progress.mud-clickable:hover:not(.mud-disabled)  { background: var(--ft-st-progress-tint); }
.mud-chip.ft-status-pending.mud-clickable:hover:not(.mud-disabled)   { background: var(--ft-st-pending-tint); }
.mud-chip.ft-status-published.mud-clickable:hover:not(.mud-disabled) { background: var(--ft-st-published-tint); }

.mud-chip.ft-status-idea.mud-clickable:hover:not(.mud-disabled),
.mud-chip.ft-status-planned.mud-clickable:hover:not(.mud-disabled),
.mud-chip.ft-status-progress.mud-clickable:hover:not(.mud-disabled),
.mud-chip.ft-status-pending.mud-clickable:hover:not(.mud-disabled),
.mud-chip.ft-status-published.mud-clickable:hover:not(.mud-disabled) { box-shadow: var(--ft-shadow-sm); }

/* ---- 9. Platform chips -----------------------------------------------
   The brand colour moves from the fill to a dot. A chip flooded with a brand
   colour outshouted every other piece of information in the row and brought
   seven colours the system does not own into the UI — Instagram arriving
   with a five-stop gradient. The dot carries the same information and leaves
   the hierarchy intact.                                                    */

/* No fill, no border — deliberate, and the third state after two measured
   attempts at a container, not the first idea tried:
     1. background: var(--ft-surface-2) — identical to TableStriped's
        alternating-row colour (see FlowtentTheme.cs). 1.000 contrast
        against itself: on a striped row the pill had no container at all.
     2. background: var(--ft-surface) — the OTHER row colour. Fixed the
        striped rows (1.11:1) but a plain MudTable row is transparent and
        inherits Surface, i.e. the SAME token — so non-striped rows dropped
        to the exact 1.000 the first attempt had, just on the other parity.
   Both candidates ARE row backgrounds — --ft-surface and --ft-surface-2
   are the only two neutrals a row can show, and the chip fill was drawn
   from that same pair both times. There is no third value hiding between
   them that reads as distinct from both; hunting one is chasing a number,
   not fixing the thing. Removing the fill removes the collision instead:
   nothing to blend into either background, on either parity. The label
   already carries the platform name at 13.6:1+ and the dot is redundant
   reinforcement — neither one needs a container to be identifiable.      */
.platform-chip.mud-chip {
    background: transparent;
    color: var(--ft-text-1);
    font-weight: 500;
}
.platform-chip.mud-chip::before {
    content: "";
    width: 8px; height: 8px;
    border-radius: var(--ft-r-full);
    margin-right: 6px;
    flex: 0 0 auto;
    background: var(--ft-chip-brand, var(--ft-text-3));
}
.platform-chip-instagram.mud-chip { --ft-chip-brand: var(--ft-brand-instagram); }
.platform-chip-facebook.mud-chip  { --ft-chip-brand: var(--ft-brand-facebook); }
.platform-chip-tiktok.mud-chip    { --ft-chip-brand: var(--ft-brand-tiktok); }
.platform-chip-linkedin.mud-chip  { --ft-chip-brand: var(--ft-brand-linkedin); }
.platform-chip-youtube.mud-chip   { --ft-chip-brand: var(--ft-brand-youtube); }
.platform-chip-twitter.mud-chip   { --ft-chip-brand: var(--ft-brand-x); }
.platform-chip-snapchat.mud-chip  { --ft-chip-brand: var(--ft-brand-snapchat); }

/* Same trap the status scale (section 8) already paid to find: MudBlazor's
   .mud-chip-filled.mud-clickable:hover:not(.mud-disabled) is (0,4,0) and
   beats .platform-chip.mud-chip at (0,2,0), so a clickable platform chip
   would revert to --mud-palette-action-disabled grey on hover — painting a
   fill onto a chip that deliberately has none. No call site passes OnClick
   today — GetBrandChipClass is applied to plain, non-interactive MudChips
   across all eight usages — but the guard is one rule, and section 8 is
   the reason we know the moment one becomes clickable is not a good moment
   to discover this. Held at transparent, not darkened: same reasoning as
   section 8 — there's no designed hover state yet, so shadow alone is the
   feedback.                                                              */
.mud-chip.platform-chip.mud-clickable:hover:not(.mud-disabled) {
    background: transparent;
    box-shadow: var(--ft-shadow-sm);
}

/* ---- 10. Layers ----------------------------------------------------------
   This rule is an override, not a first definition: MudBlazor.min.css ships
   `#blazor-error-ui{…z-index:9999}` at (1,0,0). Ours ties on specificity and
   wins on document order. Delete the bridge together with MudBlazor and the
   bar keeps 9999; delete the bridge alone and it goes back to 9999, which is
   why app.css no longer carries a value of its own.

   The failure bar has to sit above every MudBlazor overlay — and after
   FlowtentTheme remapped MudTheme.ZIndex onto the --ft-z-* scale, the
   highest of those is the tooltip at 600, not the library's stock 1600. So
   700 clears them.

   Do P3 przykrywały go jeszcze: .feedback-fab 9998, .cookie-consent-banner
   9999, .alpha-banner 10000, .flowtent-rte-mention-popup 10000,
   .mud-drawer-overlay 10005 i .mobile-nav-drawer 10010 — klaster zszedł na
   skalę razem z przebudową powłoki (sekcje 19-20 poniżej i rich-text.css).
   Reszta geometrii przeniesiona z app.css razem z tym plikiem: markup w
   Components/Shared/BlazorErrorUi.razor, kolory świadomie stałe (jasny
   ostrzegawczy pasek niezależny od motywu — color-scheme: light only). */
#blazor-error-ui {
  color-scheme: light only;
  background: #FFF4DC;
  color: #8B5A00;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  box-sizing: border-box;
  display: none;
  left: 0;
  padding: 0.6rem 2.75rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: var(--ft-z-system);
}
#blazor-error-ui .reload {
  color: #8B5A00;
  font-weight: 600;
  text-decoration: underline;
}
/* Zamknięcie paska to <button>, nie <span> (P13 Z7, wejścia P10 poz. 53).
   blazor.web.js podpina onclick po selektorze #blazor-error-ui .dismiss, bez
   względu na element, a tylko przycisk stoi w porządku Tab i reaguje na Enter
   i Spację. Reset jest jawny, choć globalne button{…} z MudBlazor.min.css robi
   dziś to samo: most nie może zależeć od tego, co biblioteka akurat zeruje
   (ten sam argument co nota o z-index wyżej). 24×24 px to minimalny cel z
   WCAG 2.2 (2.5.8) — sam glif × ma ok. 8 px szerokości. Prawy padding paska
   (2.75rem = 44 px wobec 12 + 24 px przycisku) trzyma łamiący się tekst
   komunikatu z dala od powiększonego celu. */
#blazor-error-ui .dismiss {
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px;
  margin: 0; padding: 0;
  border: 0; border-radius: var(--ft-r-sm);
  background: transparent;
  color: inherit;
  font: inherit; font-weight: 700; line-height: 1;
  cursor: pointer;
}
/* Pierścień fokusu w kolorze tekstu paska. Pasek ma kolory stałe (color-scheme:
   light only), a globalne html :focus-visible (sekcja 4) bierze --ft-focus-ring,
   który w ciemnym motywie jest #B4A2FD. Na tle #FFF4DC daje to ok. 2,0:1, czyli
   poniżej 3:1 wymaganych dla wskaźnika fokusu (w jasnym motywie #7C5CFC daje ok.
   4,0:1). #8B5A00 to kolor tekstu paska: ok. 5,4:1 w obu motywach. Reguła
   obejmuje też link .reload, bo stoi na tym samym tle z tego samego powodu. */
#blazor-error-ui .reload:focus-visible,
#blazor-error-ui .dismiss:focus-visible {
  outline: var(--ft-focus-ring-width) solid #8B5A00;
  outline-offset: var(--ft-focus-ring-offset);
}

/* ---- 11. Empty and error states -----------------------------------------
   One block for both: they are the same box with a different glyph, and
   splitting them would let the two drift apart on padding. EmptyState uses
   .ft-state-icon (56-72px, text-3, no background); ErrorState introduces
   .ft-state-badge instead (38px circle on the danger tint/deep pair) — an
   error is a distinct signal, not a mid-tone illustration.               */
.ft-state {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 28px 20px; text-align: center;
  border: var(--ft-border); border-radius: var(--ft-r-md);
}
.ft-state-icon  { color: var(--ft-text-3); }
.ft-state-title { font: var(--ft-type-h3); color: var(--ft-text-1); }
.ft-state-desc  { font: var(--ft-type-small); color: var(--ft-text-2); max-width: 42ch; }
.ft-state-badge {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: var(--ft-r-full);
  background: var(--ft-danger-tint); color: var(--ft-danger-deep);
}
/* P2/11 — danger tint/deep as used here: light 5.68:1, dark 6.70:1. The
   badge carries only an SVG icon (ErrorState.razor), so the graphical-object
   floor (3:1, SC 1.4.11) is the one that actually binds — both themes clear
   it with room to spare, and both also clear the stricter 4.5:1 text floor
   in case a future caller puts a glyph-and-letter inside instead. */

/* ---- 12. Preflight chips ------------------------------------------------
   The check's verdict, not an entity: an inline span on the semantic token
   pairs, never a MudChip. The glyph is the second signal — colour alone
   would leave pass and warn indistinguishable to a colour-blind user.     */
.ft-pf {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: var(--ft-r-full);
  font: var(--ft-type-micro); white-space: nowrap;
}
.ft-pf-pass { background: var(--ft-success-tint); color: var(--ft-success-deep); }
.ft-pf-warn { background: var(--ft-warning-tint); color: var(--ft-warning-deep); }

/* P2/11 — measured with a WCAG relative-luminance script against the hex
   pairs in tokens.css, not eyeballed:
     .ft-pf-pass  light 5.75:1  dark 7.81:1  (>= 4.5:1 text floor — PASS)
     .ft-pf-warn  light 6.07:1  dark 7.53:1  (>= 4.5:1 text floor — PASS)
   Both are the same success/warning tint-deep pairs tokens.css already
   documents at the token declaration; restated here because this is the
   rule that actually pairs them as background/foreground. */

/* The glyph must not shrink when the label wraps — same guard .ft-status
   ships for the same shape. Latent under nowrap, load-bearing under wrap. */
.ft-pf svg { flex: 0 0 auto; }

/* PreflightChecks stacks these in a ~2/12 column — 115px of text width at
   1280 — where every label is a sentence. nowrap there does not widen the
   column (the stack stretches its children); it spills the text out of the
   pill. This variant lets the label break, pins the glyph to the first
   line, and pulls the radius off r-full: at three wrapped lines a 999px
   corner resolves to 28px and clips the outer lines. */
.ft-pf-wrap {
  align-items: flex-start; white-space: normal; word-break: break-word;
  padding: 4px 8px; border-radius: var(--ft-r-md);
}

/* ---- 13. Page header ----------------------------------------------------
   h1 gets an explicit font: the browser default is 2em bold with generous
   margins, which would tower over the type scale. Margin zeroed for the same
   reason — the wrapper owns spacing.                                       */
.ft-pagehead { margin-bottom: 16px; }
/* P2/11 — text-3 on --ft-bg (the crumb sits directly on the page background,
   not a card): light 5.02:1, dark 6.24:1. Both clear 4.5:1; this is the same
   text-3/bg case tokens.css's own comment already names at the token. */
.ft-pagehead-crumb { font: var(--ft-type-micro); color: var(--ft-text-3); margin-bottom: 2px; }
/* P3: szlak z powłoki — linki dziedziczą text-3 okruszka (5.02:1 na tle,
   patrz pomiar linijkę wyżej), odróżnia je dopiero podkreślenie na
   hover/fokus. Accent w tej roli krzyczałby nad każdym h1 w aplikacji. */
.ft-pagehead-crumb a { color: inherit; text-decoration: none; }
.ft-pagehead-crumb a:hover,
.ft-pagehead-crumb a:focus-visible { text-decoration: underline; }
.ft-pagehead-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ft-pagehead-leading { display: flex; align-items: center; }
/* Wrapper wokol h1 — jeden dziecko-element flex-column nie zmienia
   renderowanego pudełka, więc strony bez Subtitle (wiekszosc) wygladaja
   identycznie jak przed P5/T9; strony Z Subtitle (Projekty) dostaja drugi
   wiersz pod h1 zamiast rozciagac caly .ft-pagehead-row do dwoch linii. */
.ft-pagehead-titlewrap { display: flex; flex-direction: column; }
.ft-pagehead-title { font: var(--ft-type-h1); color: var(--ft-text-1); margin: 0; }
.ft-pagehead-subtitle { font: var(--ft-type-small); color: var(--ft-text-3); margin-top: 3px; }
.ft-pagehead-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }

/* An empty slot still renders its wrapper div, and flex's `gap` charges for
   it same as a populated one — a gated state (e.g. notFound, no Actions
   passed) loses 12px and the h1 drifts out of line with every other page's
   title. :empty only matches a truly childless element, which is exactly
   the gated case; a slot with whitespace-only content would not match, but
   neither Leading nor Actions ever renders whitespace on its own. */
.ft-pagehead-leading:empty, .ft-pagehead-actions:empty { display: none; }

/* Sloty tytułu (P13/T2, spec §4.1). Klasy istnieją w markupie TYLKO wtedy,
   gdy strona podaje slot, więc nagłówki z samym tytułem nie dostają żadnej
   nowej reguły.
   .ft-pagehead-adornment — ołówek i chipy za h1, zawijane we własnym pudełku.
   .ft-pagehead-editor — pole zmiany tytułu w miejscu h1; rośnie jak dawny
   nagłówek szczegółów posta, w którym pole Mud miało flex 1 1 auto.
   Dwie reguły przez :has(), wyłącznie w nagłówku ze slotem: h1 z tytułem encji
   łamie także długie słowo (adres wklejony w temat wypychałby stronę na
   telefonie — bramka sondy przelania, spec §3), a klaster akcji zawija się,
   bo przyciski workflow posta szacunkowo nie mieszczą się w jednym wierszu
   przy 390 px (dawny .header zawijał wszystkie przyciski naraz). Szacunek
   na papierze — rozstrzyga sonda przelania w T7. */
.ft-pagehead-adornment { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.ft-pagehead-editor { display: flex; align-items: center; gap: 4px; flex: 1 1 auto; min-width: 0; }
.ft-pagehead-adornment:empty { display: none; }
.ft-pagehead-row:has(> .ft-pagehead-adornment) .ft-pagehead-title { overflow-wrap: anywhere; }
.ft-pagehead-row:has(> .ft-pagehead-adornment) > .ft-pagehead-actions,
.ft-pagehead-row:has(> .ft-pagehead-editor) > .ft-pagehead-actions { flex-wrap: wrap; }
/* Pole zmiany tytułu posta (PostHeaderBar, P13/T2). min-width przeniesione z
   bloku style komponentu (.rename-input), który razem z .header i .title nie
   miał zakresu i obowiązywał w całej aplikacji. W klasie pola Mud tylko
   min-width, nigdy flex-basis (ui-ux.md, Razor markup pitfalls). */
.ft-pagehead-rename { min-width: 240px; }

/* Na wąskim pasku pełne --ft-type-h1 (28px/36px) góruje; wariant mobilny
   to pełny skrót font (rozmiar + interlinia), nie sam rozmiar — inaczej
   zostaje 36px interlinii. Przeniesione z app.css (P3); wygrywa z regułą
   bazową kolejnością w pliku, ta sama specyficzność. */
@media (max-width: 959.99px) {
  .ft-pagehead-title { font: var(--ft-type-h1-mobile); }
}

/* ---- 14. Navigation progress bar ----------------------------------------
   Indeterminate on purpose: Blazor Server gives no progress figure for a
   navigation, and a fake percentage that jumps to 90% and waits is worse
   than an honest sweep.                                                   */
@keyframes ft-loadbar {
  0%   { transform: translateX(-100%) scaleX(.4); }
  50%  { transform: translateX(0) scaleX(.7); }
  100% { transform: translateX(100%) scaleX(.4); }
}
.ft-loadbar {
  position: fixed; left: 0; right: 0; height: 3px;
  top: var(--mud-appbar-height, 52px);
  overflow: hidden; background: transparent;
  /* Sticky, not snackbar. The bar belongs with the app bar it hangs under,
     and the scale above it — dialog 400, snackbar 500, tooltip 600 — is
     for things that sit ON TOP of the page. A navigation indicator painted
     over an open dialog would be reporting on a page the user has stopped
     looking at. */
  z-index: var(--ft-z-sticky);
  pointer-events: none;
}
.ft-loadbar-fill {
  height: 100%; width: 100%; transform-origin: left center;
  background: var(--ft-accent);
  animation: ft-loadbar 1.8s cubic-bezier(.2,.6,.2,1) infinite;
  /* Reduced motion: reaches for tokens.css's existing .ft-motion-essential
     hatch ("The general hatch, for our own components") instead of a
     bespoke @media block here. The .mud-progress-* carve-out in tokens.css
     exists only because that DOM belongs to MudBlazor, not to us, so a
     class can't be added to it by hand — this element is ours, so the
     general hatch is the right tool, not a second copy of the same fix.
     A frozen bar reads as a hang, same reasoning as the carve-out: restate
     the timing, do not turn the animation off (contrast .ft-shimmer above,
     where a flat plane still reads as "loading" — a stalled 3px segment
     does not). */
  --ft-motion-essential-duration: 1.8s;
}

/* ---- 15. KPI tile -------------------------------------------------------
   tabular-nums so a column of figures lines up; proportional digits make
   "11" narrower than "44" and the column looks broken. tabular-nums only
   aligns the DIGITS inside one number — it does nothing for the baseline
   BETWEEN tiles, which is the label's job below.                          */
.ft-kpi {
  background: var(--ft-surface); border: var(--ft-border);
  border-radius: var(--ft-r-lg); padding: 16px 20px;
  box-shadow: var(--ft-shadow-sm);
}
/* min-height reserves a second line for every tile, not just the ones that
   need it. Measured on Home.razor's dashboard grid: at xs=6 the Polish label
   "Oczekuje na zatwierdzenie" wraps to two lines while its four siblings stay
   on one, so ITS OWN value dropped 16px below the row it shares with them —
   tabular-nums cannot reach across tiles to fix that, it only lines up the
   digits within a single value. Reserving the wrap's height on every label
   keeps the row of values level regardless of which tile's text is longest. */
/* P2/11 — text-2 on --ft-surface (.ft-kpi's own background, declared above):
   light 7.80:1, dark 9.08:1. Both comfortably clear 4.5:1. */
.ft-kpi-label { display: flex; align-items: center; gap: 7px; min-height: 32px; font: var(--ft-type-micro); color: var(--ft-text-2); }
.ft-kpi-value { font: var(--ft-type-display); font-variant-numeric: tabular-nums; margin-top: 4px; color: var(--ft-text-1); }
.ft-kpi-context { font: var(--ft-type-small); color: var(--ft-text-3); }

/* Poniżej 360 px jeden kafel w wierszu (P14 Z3, wejścia P10 poz. 123).
   Siatka KpiRow to xs=6 — dwa kafle w wierszu od 0 px, bo MudBlazor nie ma
   progu poniżej xs — co na 320 px daje kaflowi 132 px, 90 px wewnątrz
   paddingu. Etykieta to flex: glif 20 px + odstęp 7 px + tekst, którego
   min-content to najdłuższe SŁOWO: samo „zatwierdzenie” ma 80 px, więc
   „Oczekuje na zatwierdzenie” potrzebuje 107 px, „Opublikowane” 109 px
   (zmierzone na gałęzi archiwalnej, sonda v1: .ft-kpi-label scrollWidth
   104/107/109 > 90 na 320 px). Żadne cięcie paddingu tego nie sięga — kafel
   musi dostać cały wiersz. Hak .ft-kpi-grid na MudGrid (KpiRow.razor)
   nadpisuje .mud-grid-item-xs-6 (flex-basis/max-width 50%): 0,2,0 bije 0,1,0
   biblioteki bez względu na kolejność arkuszy. Pomiar po zmianie:
   probes/overflow.mjs, szerokość 320. */
@media (max-width: 359.98px) {
  .ft-kpi-grid > .mud-grid-item-xs-6 { flex-basis: 100%; max-width: 100%; }
}

/* ---- 16. Card ----------------------------------------------------------
   Rest is a hairline plus shadow-sm, NOT Elevation=2: MudBlazor's elevation
   ramp is a Material shadow that reads as a floating sheet, and a working
   list of thirty of them looks like a pile. Lift on hover only where the
   card is actually clickable.

   No consumer yet, same as .ft-status was at the end of P1 (section 8) —
   named ahead of its call site on purpose. Clickable cards arrive with P4's
   work screens (Direction-A list cards: Ideas, Templates and similar grids
   of clickable tiles); the base, non-clickable .ft-card can be adopted by
   any plain MudPaper Elevation=1 the moment that screen gets rebuilt — it
   does not need a P4-specific reason, just a rebuild touching it.

   Hover wrapped in a pointer query. On touch, :hover latches after a tap and
   the lifted state sticks until something else is tapped — MudBlazor guards
   its own hover rules this way and an unguarded override reintroduces it.  */
.ft-card {
  background: var(--ft-surface); border: var(--ft-border);
  border-radius: var(--ft-r-lg); box-shadow: var(--ft-shadow-sm);
  transition: box-shadow var(--ft-dur-base) var(--ft-ease), transform var(--ft-dur-base) var(--ft-ease);
}
/* Composes WITH .ft-card, does not replace it — a clickable card is always
   both classes together. cursor lives outside the pointer query below: it
   has to apply on touch too, where the query's hover half never fires. */
.ft-card-clickable { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .ft-card-clickable:hover {
    box-shadow: var(--ft-shadow-md);
    transform: translateY(var(--ft-lift));
  }
}

/* ---- 17. Avatar (minimal scaffold) ----------------------------------------
   A peer of Card, not a subtopic of it — the spec table lists Avatar as its
   own [Mud + nadpisania] entry alongside Card, so it gets its own top-level
   number, unlike 6a and 7a, which are true sub-details of the sections
   before them.

   No consumer yet, and none rebuilt here. SideMetaPanel.razor's reviewer
   stack (22px) and NotificationBell.razor's actor avatar (28px) already
   render their own inline spans, and both already carry this bridge's own
   conventions before this class existed: overlap via margin-left: -4px, a
   ring in --ft-surface (not white, or the ring would glow on a dark ground),
   colour from AvatarColorHelper.For(seed). They are correct today, sit
   inside ui-ux.md's 22-32px range, and rebuilding two working call sites is
   not what this task is for.

   No consumer yet: ListRow shipped with an Author slot, but its first
   binding (ContentPostRow) does not use it, so these classes wait for the
   P4 work screens the same way .ft-card does — at the spec's 24/32/40
   sizes; tokens.css even labels --ft-r-full "pills, avatars" for a circle
   no caller has used it for yet. Shape and stacking only: the fill
   comes inline per instance from AvatarColorHelper.For(seed), one of the
   app's ten documented markup-colour exceptions (spec §0) because it is
   per-user data, not a design decision — there is no token for it to read
   here. Ink is --ft-avatar-ink (tokens.css), for the same reason
   NotificationBell's literal is: it sits on that fixed, hash-derived
   palette, which does not follow the theme.                               */
.ft-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--ft-r-full);
  border: 1.5px solid var(--ft-surface);
  color: var(--ft-avatar-ink); font-weight: 600;
  flex: 0 0 auto;
}
.ft-avatar-24 { width: 24px; height: 24px; font-size: 10px; }
.ft-avatar-32 { width: 32px; height: 32px; font-size: 12px; }
.ft-avatar-40 { width: 40px; height: 40px; font-size: 14px; }
/* Poza spec'owym zakresem 24/32/40 (ui-ux.md "Avatars": 22-32px w stosach,
   32px w gniazdach głównych) — jedyny call site to Profile.razor §26,
   makieta Profil.dc.html L46 (72px, samodzielny slot tożsamości karty
   "Dane konta", nie stos ani avatar w wierszu). Świadome rozszerzenie tej
   samej rodziny klas, nie nowy dialekt — ten sam .ft-avatar bazowy. */
.ft-avatar-72 { width: 72px; height: 72px; font-size: 24px; }

.ft-avatar-stack { display: inline-flex; }
.ft-avatar-stack .ft-avatar { margin-left: -4px; }
.ft-avatar-stack .ft-avatar:first-child { margin-left: 0; }

/* ---- 18. List row --------------------------------------------------------
   57px floor: working density is 30-50 items in frame, and taller rows mean
   scrolling to see a week's work.

   The two breakpoints fold columns away rather than swapping in a second
   layout. That is the whole reason ListRow (Components/Shared/ListRow.razor)
   exists — see its own header comment for the F14/H4 audit context.

   COLUMN CONTRACT. The five slots (main, due, author, status, actions) are
   pinned to their grid column BY CLASS, not by DOM order:

     .ft-row-main    → column 1     .ft-row-status  → column 4
     .ft-row-due     → column 2     .ft-row-actions → column 5
     .ft-row-author  → column 3

   A naive `grid-template-columns` + auto-placement would work only for a
   host that supplies all five slots in order. ContentPosts.razor (the first
   consumer, Task 10) does not use Author — with auto-placement its Status
   would land in the Author column (a narrow track, wrong width, wrong rule) and
   Actions would land in the Status column, leaving the real actions column
   empty. Pinning by class means a skipped slot leaves its own column blank
   instead of shifting every slot after it. The trade-off is visible: a host
   that never uses Author pays for it with a permanent Author-width gutter on wide
   screens rather than a dynamic reflow, which would need a host-supplied
   "which slots exist" flag on .ft-row itself. Not built — no second
   consumer has asked for it yet, and the gutter is a smaller cost than the
   complexity of a conditional template.

   The header row (.ft-row-head) reuses the same per-slot classes AND the
   same --ft-row-cols custom property below, so its labels stay pinned to
   the same columns without a second set of widths to drift from these —
   "one set of column widths" is enforced by the variable, not by a comment
   asking two rules to be kept in sync by hand. Each breakpoint below
   overrides --ft-row-cols exactly once, on the combined `.ft-row,
   .ft-row-head` selector (or `.ft-row` alone once .ft-row-head is
   display:none); grid-template-columns itself is declared exactly once,
   here, and never repeated.

   Status column is 216px, not a rounder number: measured against the two
   shipping locales (Components/Shared/StatusChip.razor's five PostStatus
   labels via SharedResource.resx/.pl.resx), N'Oczekuje na zatwierdzenie'
   (PendingApproval, PL) renders at 192px including the glyph — the widest
   of all ten EN+PL labels by a wide margin. A CSS Grid item's min-width is
   `auto` by default, so a track sized to fit the SHORT labels (152px, the
   original guess) does not clip the long one — it lets the pill overflow
   the track and paint over Actions instead, invisible until a status with a
   long label is on screen. 216px clears the measured 192px with a margin
   for font-rendering variance across browsers; verified via Playwright
   (tools/ui-screenshots), not eyeballed.                                   */
/* Tory Autor i Akcje (P13 Z6) liczone z ETYKIETY nagłówka, nie z treści
   wiersza. W stylu .ft-row-head (micro, wielkie litery, odstęp .04em)
   etykiety są szersze niż to, co stoi pod nimi (stos awatarów 24 px, kebab
   26 px), a dawne tory z makiety (40 i 36 px) wypuszczały je poza komórkę:
   Autor w odstęp przed kolumną Status, Akcje (ostatnia kolumna) w prawy
   padding wiersza. Pomiar P13/T1 przy 1440 px w obu kulturach (MudBlazor
   9.9): Author 53 px, Autor 43,2 px, Actions 56,5 px, Akcje 39,8 px.
   Reguła: większa z dwóch wartości zaokrąglona w górę do wielokrotności
   4 px, Autor nie węższy niż 40 px, Akcje nie węższe niż 44 px. Etykiety
   zostają widoczne: .ft-row-head jest aria-hidden, więc ukrycie wizualne
   nie dałoby czytnikowi niczego, a pusta komórka zabrałaby nazwę kolumny
   widzącym. Zmiana tekstu etykiety w resx wymaga nowego pomiaru. */
.ft-row, .ft-row-head {
  --ft-row-cols: minmax(240px, 1fr) 118px 56px 216px 60px;
  grid-template-columns: var(--ft-row-cols);
}
.ft-row {
  display: grid;
  gap: 12px; align-items: center;
  min-height: 57px; padding: 8px 20px;
  background: var(--ft-surface);
  border-bottom: var(--ft-border);
  transition: background var(--ft-dur-fast) var(--ft-ease);
}
.ft-row-clickable { cursor: pointer; }

@media (hover: hover) and (pointer: fine) {
  .ft-row-clickable:hover { background: var(--ft-surface-2); }
}

.ft-row-main    { grid-column: 1; display: flex; align-items: center; gap: 12px; min-width: 0; }
.ft-row-due     { grid-column: 2; }
.ft-row-author  { grid-column: 3; }
.ft-row-status  { grid-column: 4; }
.ft-row-actions { grid-column: 5; }

.ft-row-text  { min-width: 0; }
/* P2/11 — text-1 on --ft-surface (.ft-row's own background, declared above):
   light 15.17:1, dark 14.42:1. Same case tokens.css already names at the
   token; restated here because this is the rule that consumes it. */
.ft-row-title {
  font: 600 14px/20px var(--ft-font); color: var(--ft-text-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* P8 §6 poz. 23, poprawka po przeglądzie zrzutów — T9 celował w zły element.
   Zmierzony markup MudBadge 9.2 (bUnit, ContentPostRowTests):

     <div class="ft-row-title">
       <span class="mud-badge-root">TEMAT POSTU
         <span class="mud-badge-wrapper mud-badge-top right">
           <span class="mud-badge …">3</span></span></span></div>

   Tekst tytułu jest BEZPOŚREDNIM dzieckiem .mud-badge-root, a
   .mud-badge-wrapper jest jego RODZEŃSTWEM: absolutnie pozycjonowaną nakładką
   (MudBlazor: position:absolute; inset 0; width/height 100%; display:flex),
   która trzyma samą odznakę i nie niesie ani jednego znaku tytułu. Elipsa
   postawiona na wrapperze nie miała więc czego przycinać — a przy okazji
   nadpisywała mu display:flex, od którego zależy ustawienie odznaki (klasy
   mud-badge-top/right to wyrównanie flexa). Stąd na /posts @390 tytuł urywał
   się w połowie glifu bez „…", mimo że reguła „była".

   Elementem, który się przelewa, jest .mud-badge-root i to on musi nieść
   overflow + text-overflow: text-overflow renderuje się tylko na treści
   INLINE elementu, który sam ustanawia overflow, a .ft-row-title widzi tu
   wyłącznie atomowe pudełko poziomu liniowego — takie pudełko jest przycinane,
   nigdy nie dostaje wielokropka. `display:block` + `max-width:100%` zostają z
   T9: inline-block bierze szerokość od treści, więc bez nich nie byłoby
   przekroczenia do przycięcia. Odznaka nic na tym nie traci — .mud-badge-root
   jako blok ma tę samą krawędź co .ft-row-title, które i tak ma overflow:
   hidden. `white-space: nowrap` dziedziczy się z .ft-row-title.

   .mud-badge-root renderuje się TAKŻE przy Visible=false (zmierzone), więc
   reguła dotyczy KAŻDEGO wiersza Postów, nie tylko tych z nieprzeczytanymi
   komentarzami. */
.ft-row-title .mud-badge-root {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis;
}
/* Kolumna Autor bez autora (P8 §6 poz. 11). Plan pisał tu nazwę spoza słownika;
   kreska to metadana, więc --ft-text-3 — mapa w tokens.css przy --ft-decor. */
.ft-row-author-empty { color: var(--ft-text-3); }
/* overflow-wrap: anywhere na .ft-row-meta (P13 Z6) — linia meta to wolny tekst
   użytkownika (notatki, opisy, haki), a token bez okazji do złamania (adres
   URL, ciąg hashtagów) nie łamie się sam i wychodził z kolumny tytułu pod
   chip statusu (sonda przelania P13/T1 przy 390 px na /ideas: 15 px poza
   komórkę). anywhere łamie tylko słowo, które się nie mieści, i w odróżnieniu
   od break-word obniża min-content elementu, więc tor 1fr nigdy nie jest
   wymiarowany najdłuższym słowem. Zwykłe zdania bez zmian. Pin:
   ThemeShellTests.Bridge_RowMeta_BreaksTokensWithoutBreakOpportunities. */
/* P2/11 — text-3 on the row's TWO possible grounds: resting on --ft-surface,
   and on --ft-surface-2 under the `.ft-row-clickable:hover` rule above.
     light  surface 5.38:1   surface-2 4.84:1
     dark   surface 5.77:1   surface-2 5.25:1
   All four clear 4.5:1; light/surface-2 is the tightest of the four (still
   0.34 clear), consistent with tokens.css calling that exact pairing out as
   the tightest case in the whole text-3 ramp. */
.ft-row-meta   { font: var(--ft-type-micro); color: var(--ft-text-3); overflow-wrap: anywhere; }
.ft-row-due    { font: var(--ft-type-small); color: var(--ft-text-2); font-variant-numeric: tabular-nums; }
.ft-row-thumb  { width: 40px; height: 40px; border-radius: var(--ft-r-sm); overflow: hidden; flex: 0 0 auto; }

/* Optional column-header row. --ft-row-cols (declared above, on the
   combined `.ft-row, .ft-row-head` selector) is the single template both
   share; this rule only adds the header's own presentation, it does not
   redeclare grid-template-columns. Decorative only (aria-hidden): each
   ListRow already carries its own accessible name per slot, so this adds a
   scanning aid for sighted users and nothing a screen reader needs — an
   unassociated visual label with no real <table> scope behind it would be
   noise, not information, if exposed. */
.ft-row-head {
  display: grid;
  gap: 12px; padding: 4px 20px;
  font: var(--ft-type-micro); color: var(--ft-text-3);
  text-transform: uppercase; letter-spacing: .04em;
}

/* Below 1280px the author and due columns fold into the meta line — their
   information belongs in Title/MetaLine for hosts that need it visible;
   the dedicated columns are the ones that go, not the underlying data.
   Status and Actions take over the due/author columns' place in the
   3-column fold so the row stays balanced rather than leaving two blank
   tracks. Only --ft-row-cols is overridden here — grid-template-columns
   itself stays declared exactly once, above. */
@media (max-width: 1279px) {
  /* Dolna granica toru tytułu 0 zamiast 200 px (P13 Z6). Na 600 px przy
     szynie 56 px i paddingu treści 16 px wiersz ma 512 px; po paddingu
     wiersza (40), dwóch odstępach (24) i torze Status (216) tytułowi zostaje
     232 px minus tor Akcji. Przy dawnych 36 px to 196 px, czyli 4 px
     przelania na 600–603 px, a każdy piksel szerszego toru Akcji przesuwa
     tę granicę o piksel wyżej. Tytuł ma elipsę, a .ft-row-main ma
     min-width: 0, więc granica niczego nie chroniła, tylko rozpychała wiersz
     na tabletach 7-calowych (600 CSS px). Arytmetyka z planu P13/T4, pomiar
     DOM na 600 px w T7. */
  .ft-row, .ft-row-head { --ft-row-cols: minmax(0, 1fr) 216px 60px; }
  .ft-row-due, .ft-row-author { display: none; }
  .ft-row-status  { grid-column: 2; }
  .ft-row-actions { grid-column: 3; }
}

/* Below 600px only thumbnail, title, meta and the status glyph survive as
   full elements — Actions is not dropped, though: the "…" menu is the only
   way to reach Scenario/Delete on a touch screen, so it keeps its own
   column rather than folding away with due/author. .ft-row's own
   min-height: 44px is a ROW floor, not a touch-target guarantee — it says
   nothing about the actual button inside .ft-row-actions, which measured
   26×26px (Playwright, tools/ui-screenshots) before the rule below existed.
   That gap between "the row is tall enough" and "the button is big enough"
   is exactly what let the undersized target ship unnoticed the first time.

   The label is clipped, not display:none'd: it is the pill's only
   accessible name (the glyph is aria-hidden), and display:none would
   remove it from the accessibility tree along with the pixels. Same clip
   pattern as .ft-visually-hidden (section 8) — duplicated, not reused,
   because the hiding is breakpoint-scoped and the markup cannot swap
   classes by width. */
@media (max-width: 599px) {
  .ft-row { --ft-row-cols: 1fr auto 44px; padding: 10px 14px; min-height: 44px; gap: 8px; }
  .ft-row-head { display: none; }
  .ft-row-status .ft-status { padding: 5px; gap: 0; }
  .ft-row-status .ft-status-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

  /* Prawdziwy cel dotykowy: MudMenu z Icon= rysuje aktywator jako
     MudIconButton (mud-button-root.mud-icon-button), zmierzony na 26×26 px,
     czyli poniżej minimum 44 px — stąd powiększenie niżej. Tor Akcji ma na
     tym progu 44 px, dokładnie tyle co przycisk (P13 Z6). Wcześniej miał
     36 px, a wyśrodkowany przycisk rozlewał 8 px przelania po 4 px w odstęp
     i w padding wiersza: niczego widocznego nie zasłaniał, ale było to
     przelanie komórki, a przegląd nie odróżni nieszkodliwego przelania od
     takiego, które rozpycha stronę. Etykieta nagłówka nie wpływa na ten tor
     — .ft-row-head jest tu display:none. */
  .ft-row-actions { display: flex; align-items: center; justify-content: center; }
  .ft-row-actions .mud-button-root { width: 44px; height: 44px; }
}

/* ---- 19. Shell (P3) ------------------------------------------------------
   CSS powłoki — wszystko, co dotąd mieszkało w app.css, przeniesione albo
   napisane od nowa pod ShellLayout. Geometrię (52/200/56) emituje
   MudThemeProvider z LayoutProperties; ten plik tylko ją czyta.          */

/* Logo — maska, nie <img>: przeniesione z app.css razem z pomiarami.
   <img src="…svg"> renderuje SVG jako osobny dokument, w którym
   currentColor to TAMTEN dokument, nie pasek (zmierzone rgb(0,0,0)).
   print-color-adjust: maska to tło, a tła Chrome domyślnie nie drukuje. */
.ft-logo {
  display: block;
  flex: 0 0 auto;
  height: 100%;
  aspect-ratio: 201 / 200;
  margin-right: 8px;
  background-color: currentColor;
  -webkit-mask: url(/flowtent-logo.svg) center / contain no-repeat;
  mask: url(/flowtent-logo.svg) center / contain no-repeat;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

/* Wysoki kontrast: background-color pada do Canvas i logo znika (zmierzone
   Playwrightem, forcedColors: 'active'). CanvasText śledzi wybrany motyw. */
@media (forced-colors: active) {
  .ft-logo { background-color: CanvasText; }
}

/* Budżet paska na telefonie. Stała treść — marginesy 16+16, hamburger 48,
   znak 38 + margines 8 + „Flowtent” 59, dzwonek + awatar 100 — mierzy
   284 px; poza dwoma MudSpacerami wszystko jest niekurczliwe, a wyszukiwarka
   jest d-none poniżej 600 px. Na 320 px zostaje 36 px luzu. W P14 (Z3) pasek
   niósł jeszcze chip wersji (10 + 50, razem 344 px, sonda v1 na gałęzi
   archiwalnej: na 320 px .mud-toolbar scrollWidth 328 > 320), więc poniżej
   360 px napis był klipowany, a znak tracił margines. Chip zniknął przy
   przejściu na wczesny dostęp, a z nim powód tamtej reguły — napis stoi na
   każdej szerokości. Gdyby pasek znów się nie mieścił: klip, nie display:none —
   .ft-logo jest aria-hidden, więc .ft-shell-wordmark to jedyna dostępna
   nazwa linku do pulpitu. Pomiar: probes/overflow.mjs, szerokości 320 i 360. */

/* Sidebar. Etykiety sekcji i kłódki. */
.ft-shellnav-section {
  padding: 14px 16px 4px;
  font: var(--ft-type-micro); text-transform: uppercase; letter-spacing: .06em;
  color: var(--ft-text-3);
}
.ft-shellnav-locked {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px; color: var(--ft-text-3); cursor: not-allowed;
}
.ft-shellnav-lock { margin-left: auto; }
.ft-shellnav-label {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Tablet 600-960: mini — sama szerokość i schowane etykiety, ŻADNEGO
   wariantu Mini MudBlazora (ryzyko 1 specu ominięte: nie walczymy z jego
   stylami, tylko nadpisujemy jedną zmienną szerokości). Wartość 56px =
   DrawerMiniWidthLeft w FlowtentTheme — zmieniaj razem. Selektor
   html body .ft-shell.mud-layout (0,2,2) bije :root providera (0,1,0)
   niezależnie od kolejności dokumentu — .ft-shell dokłada się do
   specyficzności, nie tylko zawęża zasięg (patrz niżej, dlaczego zawężenie
   jest tu obowiązkowe).

   Etykiety schowane WIZUALNIE, nie display:none (P13 Z9, wejścia P10
   poz. 59). display:none wyjmował etykietę z drzewa dostępności, więc na
   tablecie nazwę linku niósł wyłącznie title — a ten sam title na desktopie,
   obok widocznej etykiety, czytnik czytał drugi raz jako opis. Po zdjęciu
   title z ShellNav etykieta jest jedyną nazwą linku, więc musi zostać w
   drzewie. Wzorzec klipu jak .ft-visually-hidden (sekcja 8) — zduplikowany,
   nie użyty, bo ukrycie zależy od szerokości, a markup nie podmienia klas
   (ten sam powód co ListRow, sekcja 18). position:absolute wyjmuje element z
   przepływu tak samo jak display:none, więc ikony w szynie nie drgną;
   padding:0 zdejmuje własne 14/16/4 px sekcji z pudełka 1 px. Sekcja
   Administracja (zwykły div, nie nagłówek) zostaje w drzewie z tego samego
   powodu co etykiety: czytnik na tablecie słyszy tę samą kolejność co na
   desktopie. Pomiar nazw wyrenderowanych: probes/a11y.mjs, sekcja 8.

   Ikona na środku szyny (P13 Z9, zmierzone sondą przelania w T1: obcięte
   +6 px a.mud-nav-link na każdym z 13 ekranów z szyną). MudBlazor 9.9 daje
   każdemu div.mud-nav-link-text margin-left i margin-inline-start 12px
   (.mud-nav-link .mud-nav-link-text, 0,2,0). W szynie ten div jest pusty
   (etykieta wyjęta z przepływu), ale margines zostawał: zawartość linku
   16 + 24 + 12 + 0 + 16 = 68 px w linku 56 px, wyśrodkowana, więc środek
   ikony stał na 22 px zamiast 28. Zerujemy oba marginesy (tę samą parę
   biblioteka zeruje sama w swojej nawigacji strony) — zawartość ma 56 px i
   ikona staje na środku. Selektor 0,3,0 bije regułę biblioteki bez względu
   na kolejność arkuszy; tylko w szynie, bo na 200 px margines odsuwa
   etykietę od ikony. */
@media (min-width: 600px) and (max-width: 959.98px) {
  html body .ft-shell.mud-layout { --mud-drawer-width-left: 56px; }
  .ft-shellnav-label, .ft-shellnav-section {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
    padding: 0; margin: 0;
  }
  .ft-shell-drawer .mud-nav-link { justify-content: center; }
  .ft-shell-drawer .mud-nav-link .mud-nav-link-text { margin-left: 0; margin-inline-start: 0; }
}

/* Telefon: szuflada temporary zaczyna się pod paskiem — scrim nie zakrywa
   paska ani paska postępu (D4; koniec klastra 10005/10010, warstwa szuflady
   i scrimu przychodzi z motywu: --ft-z-drawer 300).

   .ft-shell na obu selektorach, nie tylko na .ft-shell-drawer: gołe
   .mud-drawer-overlay dopasowywało też scrim STAREJ powłoki
   (DashboardLayout.razor, żywej do cutoveru — Task 10), więc telefon na
   starej powłoce dostawał obcy 52px offset na scrimie, którego jej własny
   .mud-drawer-overlay{z-index:10005} w app.css nigdy nie zamawiał. Marker
   klasy na MudLayout nowej powłoki (ShellLayout.razor) izoluje regułę do
   drzewa, w którym faktycznie renderuje się .ft-shell-drawer. */
@media (max-width: 599.98px) {
  .ft-shell .ft-shell-drawer.mud-drawer {
    top: var(--mud-appbar-height, 52px);
    height: calc(100vh - var(--mud-appbar-height, 52px));
  }
  .ft-shell .mud-drawer-overlay { top: var(--mud-appbar-height, 52px); }
}

/* Treść: 1600 px (--ft-content-max) wycentrowane + sticky footer — dwa
   wzorce starej powłoki w jednym: .ft-shell-main jest kolumną flex na całą
   wysokość, body rośnie (flex:1), stopka siedzi na dnie krótkich stron. */
.ft-shell-main {
  min-height: 100vh;
  display: flex; flex-direction: column;
  background-color: var(--ft-bg);
  padding: calc(var(--mud-appbar-height, 52px) + 16px) 16px 16px;
}
.ft-shell-body {
  flex: 1;
  width: 100%; max-width: var(--ft-content-max);
  margin: 0 auto;
}
.ft-shell-main > footer { flex-shrink: 0; }

/* Przełącznik projektu w pasku — geometria przeniesiona z app.css
   (.navbar-project-select), sama nazwa klasy nowa. */
.mud-select:has(.ft-shell-projselect) { width: 500px; flex-grow: 0; }
/* Tablet: 500 px rozpychalo pasek (zmierzone na zrzutach 768 px — szukajka,
   dzwonek i awatar wypadaly za krawedz). Szerokosc w moscie, nie inline,
   zeby mogla oddychac z viewportem. */
@media (min-width: 600px) and (max-width: 959.98px) {
  .mud-select:has(.ft-shell-projselect) { width: 200px; }
}
.ft-shell-projselect .mud-input-slot { text-align: center; }

/* Cookie consent i FAB opinii — na skali --ft-z-*; wartości 9999/9998
   umarły z banerem alpha, który wymuszał licytację dziesiątkami tysięcy. */
.cookie-consent-banner {
  position: fixed; bottom: 0; left: 0; right: 0;
  z-index: var(--ft-z-snackbar);
  padding: 0 16px 16px;
}
/* Dopóki zgoda cookie nierozstrzygnięta, treść oddaje banerowi pas na dole
   ekranu — inaczej fixed baner nakrywa legendy wykresów Pulpitu i stopkę
   na stronach o wysokości viewportu (zmierzone na zrzutach P3, wejścia P4).
   Budżety z pomiaru realnych wysokości banera (typografia motywu + string
   PL + pa-4): 480px → 136px, 414px → 157px, 375px → 179px, 320px → 200px.
   flex-wrap zrzuca przycisk pod tekst poniżej ~430px, stąd trzeci próg. */
body.ft-consent-open .ft-shell-main { padding-bottom: 96px; }
@media (max-width: 760px) {
  body.ft-consent-open .ft-shell-main { padding-bottom: 148px; }
}
@media (max-width: 480px) {
  body.ft-consent-open .ft-shell-main { padding-bottom: 208px; }
}
.feedback-fab {
  position: fixed; bottom: 24px; right: 24px;
  z-index: var(--ft-z-sticky);
}
/* Zapas pod FAB-em opinii (P9 §5 poz. 5 — rozwiązanie Materiału: FAB zostaje
   przyklejony, treść dostaje pas pod spodem). Bez tego ostatni element strony
   leży POD przyciskiem i nie da się go odsłonić przewijaniem: kosz w
   Ustawieniach, wartość na Pulpicie, kebab ostatniego wiersza listy i link
   „Pliki cookie" w stopce (zmierzone P8/T12). 88 px = wysokość FAB-a + 24 px
   jego `bottom` + zapas; MudFab Size.Medium mierzy 48 px (.mud-fab-size-medium
   w MudBlazor 9.2), więc zapas wychodzi 16 px, nie 8 px z liczby 56 px
   przyjętej w spec §5 — wartość 88 px zostaje taka, jak ją spec ustala.
   Nakładanie FAB-a na treść W TRAKCIE przewijania zostaje — to standardowe
   zachowanie FAB-a, pas dotyczy KOŃCA strony.

   Przy otwartym banerze zgody FAB dodatkowo podjeżdża o wysokość banera (te
   same budżety 148/208 px, które kilka linii wyżej rezerwuje padding treści) —
   dotąd leżał POD banerem, bo --ft-z-sticky (100) < --ft-z-snackbar (500).
   Padding przy otwartym banerze = wysokość banera + te same 88 px.

   Bloki MUSZĄ stać po regułach `body.ft-consent-open .ft-shell-main` wyżej:
   mają tę samą specyficzność, więc rozstrzyga kolejność w pliku.
   `padding-bottom` nadpisuje tylko dolną składową skrótu `padding` z §19. */
@media (max-width: 760px) {
  .ft-shell-main { padding-bottom: 88px; }
  body.ft-consent-open .ft-shell-main { padding-bottom: calc(148px + 88px); }
  body.ft-consent-open .feedback-fab { bottom: calc(148px + 24px); }
}
@media (max-width: 480px) {
  body.ft-consent-open .ft-shell-main { padding-bottom: calc(208px + 88px); }
  body.ft-consent-open .feedback-fab { bottom: calc(208px + 24px); }
}

/* ---- 20. Page components (legacy app.css) --------------------------------
   Nie-powłokowe reguły bez lepszego domu, dopóki ich strony nie przejdą
   P4-P6. Przeniesione bez zmian wartości.                                */

/* Tekst bogaty w trybie odczytu (P14 Z3): notatki pomysłu, komentarze posta
   (.rich-text-display, arkusz rich-text.css) i treść scenariusza
   (.rich-text-content, <style> w ContentPostDetail.razor) to HTML z edytora,
   czyli wolny tekst użytkownika — a token bez okazji do złamania (adres,
   ciąg hashtagów, długie słowo) sam się nie złamie. Zmierzone sondą
   probes/overflow.mjs na ideadetail: na 320 px `h1` w div.rich-text-display
   wewnątrz +57 px i span w tym samym bloku poza +25 px (dokument 345 > 320),
   na 360 px `h1` +17 px. Ta sama klasa wady co .ft-row-meta (sekcja 18, P13):
   `anywhere` łamie tylko słowo, które się nie mieści, i w odróżnieniu od
   `break-word` obniża min-content, więc żaden tor 1fr nie jest rozpychany
   najdłuższym słowem. Na kontenerze i każdym potomku, bo overflow-wrap
   dziedziczy się tylko do pierwszej reguły, która go nadpisze — nagłówek czy
   komórka tabeli nie może tego cofnąć. min-width: 0 na kontenerze nie jest
   potrzebne: notatki stoją w MudPaper w MudItem, a element flex MudBlazora ma
   max-width: 100%, które przycina automatyczne minimum; zresztą `anywhere`
   samo obniża min-content do szerokości znaku. Edytor ma tę regułę od dawna
   (.ProseMirror w rich-text.css) — brakowało jej po stronie odczytu. */
.rich-text-display,
.rich-text-display *,
.rich-text-content,
.rich-text-content * { overflow-wrap: anywhere; }

/* Konsola audit-log */
.audit-log-console {
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
  background-color: var(--mud-palette-surface);
}
.audit-log-entry { padding: 6px 0; border-bottom: 1px solid var(--mud-palette-lines-default); }
.audit-log-entry:last-child { border-bottom: none; }
.audit-log-time { color: var(--mud-palette-text-secondary); font-size: 0.8rem; white-space: nowrap; }
.audit-log-user { font-weight: 600; }

/* !important nie jest tu na wyrost: MudBlazor koloruje przez klasy z
   !important (.mud-inherit-text{color:inherit !important} przy
   Color.Inherit, tak w ProjectSettings przy połączonych platformach), a
   deklaracja bez !important NIGDY nie wygra z !important. Remis dwóch
   !important rozstrzyga specyficzność: (0,2,0) marki bije (0,1,0). */
/* Kolory marek ikon platform (konsument: GetBrandIconClass). */
.platform-icon.mud-icon-root { color: inherit !important; }
.platform-icon-facebook.mud-icon-root { color: #1877F2 !important; }
.platform-icon-linkedin.mud-icon-root { color: #0A66C2 !important; }
.platform-icon-youtube.mud-icon-root { color: #FF0000 !important; }
.platform-icon-tiktok.mud-icon-root { color: #FE2C55 !important; }
.platform-icon-instagram.mud-icon-root { color: #E4405F !important; }
.platform-icon-twitter.mud-icon-root { color: #1DA1F2 !important; }
.platform-icon-snapchat.mud-icon-root { color: #F4B400 !important; }

/* Selektor języka (AuthLayout) — wrapper MudSelect. */
.mud-select:has(.navbar-language-select) { max-width: 100px; flex-grow: 0; }

/* Karty połączeń social w Ustawieniach — równe wysokości, akcje na dnie. */
.social-connection-card { height: 100%; display: flex; flex-direction: column; }
.social-connection-card .mud-card-content { flex: 1 1 auto; }
.social-connection-card .mud-card-actions { margin-top: auto; }

/* SortableJS — stany przeciągania wierszy mediów. */
.media-row-ghost { opacity: 0.4; background: var(--mud-palette-action-default-hover); }
.media-row-chosen { cursor: grabbing; }
.media-row-drag { opacity: 0.9; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }

/* Stepper na telefonie — tylko aktywna etykieta. */
@media (max-width: 959.99px) {
  .mud-stepper .mud-stepper-nav .mud-step .mud-step-label .mud-step-label-content-title { display: none; }
  .mud-stepper .mud-stepper-nav .mud-step.active .mud-step-label .mud-step-label-content-title {
    display: block; font-size: 0.75rem;
  }
  /* !important: biblioteczny .mud-stepper-nav-scrollable .mud-step ma
     (0,4,0) min-width: fit-content */
  .mud-stepper .mud-stepper-nav .mud-step { min-width: 0 !important; }
  .mud-stepper .mud-stepper-nav .mud-stepper-nav-connector { min-width: 16px; }

  /* ScenarioEditor.razor:25 — jedyny konsument .page-title; skalowane w
     dół razem z resztą klastra mobilnego (P3, przeniesione z app.css bez
     zmiany wartości, !important włącznie). */
  .page-title { font-size: 1.25rem !important; }
}

/* ---- 21. Dashboard (P4/13) ------------------------------------------------
   HomeV2's four new subcomponents (AttentionBand, WeekBars, PipelineHealth,
   the mobile stats collapse) — everything else on the new dashboard reaches
   for classes sections 6/8/11/15/17 already ship (skeleton, StatusChip,
   EmptyState/ErrorState, KpiTile, avatar). */

/* Pas uwagi — karta z lewą krawędzią ostrzegawczą. Tło i krawędź z pary
   warning tint/deep (ta sama para co PreflightChip's warn, sekcja 12) —
   uwaga o zbliżającym się terminie jest tego samego rodzaju sygnału. */
.ft-attention {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 12px;
  background: var(--ft-warning-tint); border-left: 3px solid var(--ft-warning-deep);
  border-radius: var(--ft-r-md); padding: 12px 16px; margin-bottom: 8px;
}
.ft-attention:last-child { margin-bottom: 0; }
.ft-attention-text { color: var(--ft-text-1); font: var(--ft-type-small); }
.ft-attention-meta { color: var(--ft-text-2); font: var(--ft-type-micro); display: block; margin-top: 2px; }

/* Słupki tygodni — zastępują MudChart StackedBar z trendem publikacji.
   Kontener wysokości stałej (--ft-weekbars-h): procentowa wysokość słupka
   liczona jest w Razor (JS/inline style), CSS daje tylko kształt i kolor.
   Bieżący tydzień ma DWIE warstwy: obrys (cały zaplanowany zakres) +
   wypełnienie (rzeczywiście opublikowane) — zamknięte tygodnie tylko jedną
   scaloną wypełnioną warstwę, bo tam "zaplanowane" już się nie liczy. */
.ft-weekbars { display: flex; align-items: flex-end; gap: 12px; padding: 4px 0; }
.ft-weekbars-col { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }
.ft-weekbars-value { font: var(--ft-type-small); font-weight: 600; color: var(--ft-text-1); font-variant-numeric: tabular-nums; }
.ft-weekbars-value-extra { font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-weekbars-track {
  --ft-weekbars-h: 96px;
  width: 100%; height: var(--ft-weekbars-h);
  display: flex; align-items: flex-end; justify-content: center;
}
.ft-weekbars-bar {
  width: 60%; min-height: 2px;
  border-radius: var(--ft-r-sm) var(--ft-r-sm) 0 0;
  background: var(--ft-accent);
}
/* Bieżący tydzień: obrys do zaplanowanej wysokości (Published+Planned),
   wypełnienie w środku tylko do wysokości Published — luka między górą
   wypełnienia i górą obrysu JEST tym, co jeszcze zaplanowane, nie
   opublikowane. */
.ft-weekbars-outline {
  background: transparent; border: 1.5px solid var(--ft-hairline);
  display: flex; align-items: flex-end; justify-content: center;
  box-sizing: border-box;
}
.ft-weekbars-outline .ft-weekbars-fill {
  width: 100%; background: var(--ft-accent);
  border-radius: inherit;
}
.ft-weekbars-label { font: var(--ft-type-micro); color: var(--ft-text-3); white-space: nowrap; }
/* Dwie etykiety tygodnia, jedna widoczna (P14 Z3, wejścia P10 poz. 123).
   Pełny zakres `01–07.09` ma 56 px; pięć kolumn z odstępami 12 px mieści go
   od ok. 400 px (na 390 px kolumna ma 55,6 px — w tolerancji sondy, nie w
   etykiecie), niżej etykiety sąsiednich kolumn nachodziły na siebie
   (zmierzone na gałęzi archiwalnej, sonda v1: .ft-weekbars-col scrollWidth
   49 > 42 na 320 px, 53 > 50 na 360 px). Poniżej 400 px rysujemy sam
   początek tygodnia (`01.09`, 34 px).
   Dostępność inaczej niż w archiwum: tam bliźniak dostawał display:none, więc
   czytnik na telefonie słyszał sam dzień startu. Tu krótka etykieta jest
   aria-hidden (WeekBars.razor) — dekoracja dla oka — a pełny zakres poniżej
   400 px jest KLIPOWANY wzorcem .ft-visually-hidden (sekcja 8), więc czytnik
   na każdej szerokości słyszy jedno i to samo: pełny zakres. Pudełko 1 px z
   overflow:hidden sonda przelania pomija z założenia (probes/overflow.mjs). */
.ft-weekbars-label-short { display: none; }
@media (max-width: 399.98px) {
  .ft-weekbars-label-full {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .ft-weekbars-label-short { display: inline; }
}

/* Zdrowie pipeline'u — cztery paski MudProgressLinear kolorowane wg progu,
   NIE przez Color= (paleta MudBlazor nie ma czwartego, ostrzegawczego
   odcienia bez konfliktu z sekcją 2). Selektor .mud-progress-linear-color-*
   .mud-progress-linear-bar (biblioteczny wzorzec, zweryfikowany w
   MudBlazor.min.css) ma tę samą specyficzność (0,2,0) — !important
   rozstrzyga zamiast polegać wyłącznie na kolejności w kaskadzie. */
.ft-health-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.ft-health-label { flex: 0 0 auto; width: 40%; font: var(--ft-type-small); color: var(--ft-text-2); }
.ft-health-row .mud-progress-linear { flex: 1 1 auto; }
.ft-health-value { flex: 0 0 auto; min-width: 3ch; text-align: right; font: var(--ft-type-small); font-variant-numeric: tabular-nums; color: var(--ft-text-1); }
.ft-health-good .mud-progress-linear-bar { background-color: var(--ft-success-deep) !important; }
.ft-health-mid  .mud-progress-linear-bar { background-color: var(--ft-st-progress-deep) !important; }
.ft-health-low  .mud-progress-linear-bar { background-color: var(--ft-warning-deep) !important; }
.ft-health-none .mud-progress-linear-bar { background-color: var(--ft-hairline) !important; }

/* Sekcja statystyk okresu — na mobile domyślnie zwinięta. <details> natywnie:
   zamknięty bez atrybutu open (mobile-first), a od 600px w górę CSS wymusza
   zawartość zawsze widoczną i chowa strzałkę summary — desktop nigdy nie
   widzi stanu zwiniętego, bez JS i bez stanu w C# (plan P4/13 wprost: "stan
   lokalny, bez persystencji" — natywne <details> to spełnia bez pisania
   żadnego stanu). */
.ft-stats-collapse { margin-top: 12px; }
.ft-stats-summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 6px;
  font: var(--ft-type-small); font-weight: 600; color: var(--ft-text-2);
  padding: 8px 0;
}
.ft-stats-summary::-webkit-details-marker { display: none; }
/* Własny chevron zamiast zdjętego markera natywnego (P9 §4.3): dwie reguły
   wyżej gaszą marker na KAŻDEJ szerokości, a chowamy <summary> dopiero od
   600 px — poniżej zostawał klikalny nagłówek bez sygnału rozwijania. SVG
   siedzi w markupie (Home.razor) wg precedensu ProjectCard.razor; stąd flex
   + gap 6px na samym <summary>, żeby tekst i strzałka stały w jednej linii.
   `display: flex` NIE psuje desktopu: `display: none` z bloku min-width:600px
   poniżej ma tę samą specyficzność i stoi PÓŹNIEJ w pliku, więc wygrywa.
   Obrót w CSS na `details[open]`, bo <details> nie ma tu stanu w C#. */
.ft-stats-summary-chevron { flex: 0 0 auto; transition: transform 120ms; }
details[open] > .ft-stats-summary .ft-stats-summary-chevron { transform: rotate(180deg); }
@media (min-width: 600px) {
  .ft-stats-summary { display: none; }
  /* Stary silnik: <details> zamknięty ukrywa dzieci przez UA display:none
     na samym elemencie potomnym — nadpisanie display:block na
     .ft-stats-content wystarcza (poniższa linia). Nowy Chromium (silnik
     harnessu zrzutów, kanał msedge): zamknięty <details> ukrywa CAŁĄ
     zawartość przez pseudo-element ::details-content z
     content-visibility:hidden, NIE przez display na dzieciach — nadpisanie
     display na .ft-stats-content jest wtedy bez znaczenia, bo rodzic
     (::details-content) i tak ma content-visibility:hidden. Bez poniższej
     reguły sekcja statystyk znika całkowicie na desktop/tablet mimo
     "działającego" selektora — zmierzone na każdym zrzucie ≥600px (P4/14). */
  .ft-stats-collapse::details-content {
    content-visibility: visible !important;
    display: block !important;
  }
  .ft-stats-collapse .ft-stats-content { display: block !important; }
}

/* ---- 22. Auth (P5) --------------------------------------------------------
   AuthLayout.razor + Login/Register.razor — dwukolumnowa powłoka wg makiety
   mockups/flowtent-design/project/Logowanie.dc.html. Konsumowana też
   (bez zakładek/mikro-linii) przez ForgotPassword/ResetPassword/ConfirmEmail/
   RegisterConfirmation — te cztery ekrany dostają własny restyle w T7, ale
   MUSZĄ renderować się sensownie w tej powłoce już teraz (PageHeadingTests +
   PasswordResetTests to dowodzą). */

:root {
  /* Hex, nie token w tokens.css — oba przystanki istnieją WYŁĄCZNIE dla
     gradientu panelu brandowego, nie są częścią słownika barw produktu.
     OBA są stałe (nie tylko -end): panel brandowy ma zawsze biały tekst na
     fioletowym tle, niezależnie od motywu aplikacji — użycie tu motywowanego
     --ft-accent-strong było błędem znalezionym w przeglądzie (F4): w dark
     mode ten token to #AA96FD (jasny fiolet), więc biały tekst na starcie
     gradientu spadał do ok. 2.47:1, poniżej WCAG AA. -start jest tym samym
     odcieniem co --ft-accent-strong w jasnym motywie (#6843FC) — wizualnie
     bez zmiany w light, ale odpięte od motywu w dark. */
  --ft-auth-aside-start: #6843FC;
  --ft-auth-aside-end: #2A1470;
}

.ft-auth {
  min-height: 100vh;
  display: grid;
  grid-template-columns: minmax(360px, 44%) 1fr;
  background: var(--ft-bg);
  color: var(--ft-text-1);
}

.ft-auth-aside {
  background: linear-gradient(160deg, var(--ft-auth-aside-start) 0%, var(--ft-auth-aside-end) 100%);
  /* var(--ft-avatar-ink) niesie ten sam "biały, nie-motywowany" kontrakt co
     inicjały awatarów (ui-ux.md "Avatars") — panel brandowy nie podąża za
     jasny/ciemny, więc token zamiast gołego hex w .razor/.css. rgba(255,255,
     255,N) niżej w tej sekcji (pigułka wczesnego dostępu, hero-opis, stopka)
     to ŚWIADOME warianty alfy TEGO SAMEGO stałego bieli — bez odpowiadającego tokenu na
     przezroczystość, ale ta sama logika "stały kolor na stałym tle". */
  color: var(--ft-avatar-ink);
  padding: 48px;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
}
/* max-width + height: auto (P14 Z3): dekoracja ma 420 px i stoi 80 px za
   prawą krawędzią panelu, więc jej lewa krawędź wypada na szerokość panelu
   minus 340 px — poniżej 340 px przed lewą krawędzią okna. Na 320 px sonda
   (probes/overflow.mjs, narrow320, login i register, oba motywy) mierzyła
   svg.ft-auth-aside-deco „poza +20 px [lewa krawędź]”; na 360 i 390 px nic.
   Panel i tak ją przycina (overflow: hidden), ale reguła lewej krawędzi w
   sondzie przycięcia nie modeluje, a wyjątek zamierzone na +20 px byłby
   ślepy na każdą prawdziwą treść wsuniętą w lewą krawędź panelu. Tu
   dekoracja nie jest szersza niż panel + 80 px: od 340 px w górę nic się nie
   zmienia (420 ≤ szerokość + 80), niżej koło skaluje się proporcjonalnie
   (viewBox, height: auto — atrybut height="420" przestaje trzymać wysokość)
   i jego lewa krawędź staje dokładnie na krawędzi panelu. Prawa strona
   zostaje +80 px, więc wyjątek #1 sondy (obcięte +80) mierzy to samo. */
.ft-auth-aside-deco {
  position: absolute; right: -80px; bottom: -80px; opacity: .14;
  pointer-events: none;
  max-width: calc(100% + 80px); height: auto;
}
.ft-auth-brand { display: flex; align-items: center; gap: 12px; }
.ft-auth-logo { width: 40px; height: 40px; }
.ft-auth-wordmark { font: 700 22px/28px var(--ft-font); letter-spacing: -0.01em; }
.ft-auth-access-pill {
  font: var(--ft-type-micro);
  background: rgba(255, 255, 255, .16); color: var(--ft-avatar-ink);
  border-radius: var(--ft-r-full); padding: 2px 9px;
}
.ft-auth-hero { margin: auto 0; max-width: 40ch; }
.ft-auth-hero h2 {
  font: 700 32px/42px var(--ft-font); letter-spacing: -0.015em; margin: 0;
  text-wrap: pretty;
}
.ft-auth-hero p {
  font: 400 15px/25px var(--ft-font); color: rgba(255, 255, 255, .75);
  margin: 16px 0 0; text-wrap: pretty;
}
.ft-auth-dots { display: flex; gap: 8px; margin-top: 24px; }
.ft-auth-dot { width: 10px; height: 10px; border-radius: 999px; }
.ft-auth-foot { font: var(--ft-type-micro); color: rgba(255, 255, 255, .55); }

.ft-auth-main {
  display: grid; place-items: center;
  /* Jawny tor minmax(0, 1fr): niejawny tor `auto` brał min-content karty
     (380px) i rósł ponad content-box main na 390px (karta 380 + padding 2×20
     = 400 > 390, scrollWidth 400 — sonda DOM P7c/T9, wejścia P7 poz. 36);
     max-width:100% karty nie działało, bo procent w auto-torze jest
     cykliczny. Na desktopie bez zmiany: karta 380px centrowana w torze. */
  grid-template-columns: minmax(0, 1fr);
  padding: 48px 32px;
}
.ft-auth-card-col { width: 380px; max-width: 100%; }
.ft-auth-lang { display: flex; justify-content: center; margin-top: 32px; }

/* < 900px (makieta): panel zwija się do kompaktowego paska — hero i stopka
   znikają, siatka wraca do jednej kolumny. */
@media (max-width: 899.98px) {
  /* minmax(0, 1fr), nie gołe 1fr: `1fr` to `minmax(auto, 1fr)`, a `auto` bierze
     min-content kolumny — kartę 380px plus 2×20px paddingu .ft-auth-main = 420px.
     Na 390px kolumna rosła do 420px (aside i main po 420, scrollWidth 421 vs
     clientWidth 390 — zmierzone sondą DOM w P7c/T9, wejścia P7 poz. 36), a
     max-width:100% karty nie miało czego ograniczać, bo procent w auto-torze
     jest cykliczny. Z minimum 0 tor = viewport, karta = 350px. */
  .ft-auth { grid-template-columns: minmax(0, 1fr); }
  .ft-auth-aside { padding: 20px 28px; }
  .ft-auth-hero, .ft-auth-foot { display: none; }
  .ft-auth-main { padding: 28px 20px; }
}

/* Naglowek karty, wspolny dla wszystkich szesciu ekranow auth (Login,
   Register, ForgotPassword, ResetPassword, ConfirmEmail,
   RegisterConfirmation) — wydzielone z inline style powtarzanego 8x
   (przeglad F1 po T7, commit 5309eed: AuthLayout.razor sam zastrzega
   "klasy strukturalne w bridge'u, nie inline style", restyle T7 zostawil
   to jedno miejsce niedomkniete). Wariant mobilny lustrzany do
   .ft-pagehead-title (sekcja 13) — ten sam kontrakt --ft-type-h1/
   --ft-type-h1-mobile, ten sam prog. */
.ft-auth-h1 { font: var(--ft-type-h1); letter-spacing: -0.01em; margin: 0; color: var(--ft-text-1); }
@media (max-width: 959.99px) {
  .ft-auth-h1 { font: var(--ft-type-h1-mobile); }
}

/* Wrapper ikona+naglowek wysrodkowany — ekrany statusu (ConfirmEmail,
   RegisterConfirmation, ResetPassword po sukcesie), wydzielone z inline
   powtarzanego 3x. */
.ft-auth-icon-heading { display: flex; flex-direction: column; align-items: center; gap: 12px; text-align: center; }

/* Link powrotu do logowania pod karta — wszystkie szesc ekranow auth,
   wydzielone z inline powtarzanego 6x. */
.ft-auth-back-link { text-align: center; margin-top: 16px; }

/* Baner zgód in-flow na auth od tego commita — patrz errata P5/T13.
   Rezerwa paddingu (wzorzec §19) była tu STRUKTURALNIE niewystarczająca:
   działa tylko dla treści zakotwiczonej do dołu kontenera, a
   .ft-auth-main{place-items:center} nie ma luzu do rozdania, gdy karta
   (Rejestracja, 8 pól) sama przekracza 100vh — position:fixed baner
   trwale nakrywał CTA (~95% na 390px, scrollTop=0). Naprawa: CookieConsent
   dostaje Inline="true" w AuthLayout.razor i renderuje się jako zwykły
   blok na końcu .ft-auth-card-col (po stopce) zamiast position:fixed —
   in-flow nie może nigdy nakryć treści nad sobą. Klasa .ft-consent-open
   na <body> w ogóle nie jest ustawiana w trybie Inline, więc reguł rezerwy
   dla .ft-auth już tu nie ma. Style samego banera w trybie Inline — patrz
   .ft-consent-inline niżej. */
.ft-consent-inline { margin-top: 16px; }

/* Zakładki Logowanie/Rejestracja — prawdziwe kotwice <a>, nie MudMenu
   (lekcja P4: linki asertowane w prerenderze muszą być <a href>). Stan
   aktywny wynika z tego, KTÓRA strona się renderuje (Login.razor vs
   Register.razor) — bez JS, bez stanu klienckiego. */
.ft-auth-tabs {
  display: inline-flex; border: var(--ft-border); border-radius: var(--ft-r-md);
  overflow: hidden; font: var(--ft-type-micro); margin-bottom: 24px;
}
.ft-auth-tab {
  padding: 7px 16px; color: var(--ft-text-2); background: transparent;
  /* Zwykłe none, bez !important — zweryfikowane w przeglądzie (F5): nic w
     bridge/tokens ustawia bazowego podkreślenia linków silniej niż klasa
     (0,1,0), sam UA default to element selector (0,0,1). Ten sam wzorzec
     bez !important co .ft-pagehead-crumb a wyżej. */
  text-decoration: none; cursor: pointer;
}
.ft-auth-tab + .ft-auth-tab { border-left: var(--ft-border); }
.ft-auth-tab-active { background: var(--ft-accent-tint); color: var(--ft-accent-text); }

.ft-auth-field { display: flex; flex-direction: column; gap: 5px; font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-auth-input {
  border: var(--ft-border); border-radius: var(--ft-r-md); padding: 11px 14px;
  font: var(--ft-type-body); color: var(--ft-text-1); background: var(--ft-surface);
  outline: none; transition: border-color 120ms, box-shadow 120ms; width: 100%;
  box-sizing: border-box; font-family: var(--ft-font);
}
/* Focus ring z makiety — para akcent/akcent-tint, NIE ogólnoproduktowy
   html:focus-visible (sekcja 4): ten ring jest zawsze widoczny na focus
   zwykły, nie tylko :focus-visible, bo pola formularza auth to jedyna
   interakcja na ekranie i mysz vs klawiatura nie robi tu różnicy. */
.ft-auth-input:focus {
  border-color: var(--ft-accent); box-shadow: 0 0 0 3px var(--ft-accent-tint);
}
.ft-auth-pwrow {
  display: flex; align-items: center; border: var(--ft-border);
  border-radius: var(--ft-r-md); background: var(--ft-surface); padding-right: 6px;
}
.ft-auth-pwrow:focus-within { border-color: var(--ft-accent); box-shadow: 0 0 0 3px var(--ft-accent-tint); }
.ft-auth-pwrow .ft-auth-input {
  flex: 1; border: none; background: transparent; padding-right: 8px;
  border-radius: var(--ft-r-md); min-width: 0;
}
.ft-auth-pwrow .ft-auth-input:focus { box-shadow: none; }
.ft-auth-eye {
  width: 34px; height: 34px; border: none; background: transparent;
  color: var(--ft-text-3); cursor: pointer; border-radius: 7px;
  display: grid; place-items: center; flex: none;
}
.ft-auth-eye:hover { background: var(--ft-surface-2); }
.ft-auth-eye svg { width: 17px; height: 17px; }

.ft-auth-check-row { display: flex; align-items: center; justify-content: space-between; font: var(--ft-type-small); }
.ft-auth-check-label { display: flex; align-items: center; gap: 8px; color: var(--ft-text-2); cursor: pointer; }
.ft-auth-check-label-start { align-items: flex-start; }
.ft-auth-check {
  width: 17px; height: 17px; margin: 0; flex-shrink: 0;
  accent-color: var(--ft-accent-strong);
}
.ft-auth-check-label-start .ft-auth-check { margin-top: 1px; }

.ft-auth-cta {
  min-height: 46px; background: var(--ft-accent-strong); color: var(--ft-on-accent);
  border: none; border-radius: var(--ft-r-md); font: 600 15px/20px var(--ft-font);
  cursor: pointer; transition: filter 120ms; margin-top: 4px; width: 100%;
}
.ft-auth-cta:hover { filter: brightness(1.1); }
/* --ft-text-3, bo to jest czytelny tekst („Chronione przez Cloudflare
   Turnstile", „Potwierdzenie przyjdzie e-mailem"), a nie dekoracja — mapa
   ze starej nazwy z makiet siedzi w tokens.css przy --ft-decor. */
.ft-auth-micro { font: var(--ft-type-micro); color: var(--ft-text-3); text-align: center; }

/* Miernik siły hasła — 3 segmenty. Kolor niesie postęp (weak/medium/strong),
   ale KAŻDY stan ma też swój tekst w regionie aria-live obok (ui-ux.md "DON'T
   rely on color alone") — segmenty same są aria-hidden. */
.ft-pw-meter { display: flex; gap: 4px; margin-top: 6px; }
.ft-pw-meter-seg { flex: 1; height: 3px; border-radius: 2px; background: var(--ft-hairline); }
.ft-pw-meter-seg.ft-pw-meter-weak { background: var(--ft-danger-deep); }
.ft-pw-meter-seg.ft-pw-meter-medium { background: var(--ft-warning-deep); }
.ft-pw-meter-seg.ft-pw-meter-strong { background: var(--ft-success-deep); }

/* ---- 23. Projects (P5, H10 wdrozone P7a/T2) --------------------------------
   ProjectList.razor + ProjectCard.razor — karty z agregatami wg makiety
   mockups/flowtent-design/project/Projekty.dc.html (spec P5 §5). Chipy
   Aktywne/Zarchiwizowane (H10, spec P7a §4) zastapily stary przycisk +
   dialog "Zarchiwizowane" (GetArchivedProjectsQuery) — archiwum to teraz
   tryb widoku TEJ SAMEJ listy kart, nie osobny ekran. */

/* Cap 1120px na treści ekranu — świadomy WYJĄTEK od zakazu page-cap w
   ui-ux.md "Page Layout" (shell już capuje na 1600px od P3): Projekty to
   ekran przeglądowy, makieta rysuje go węziej niż zwykła strona robocza.
   Nota w ui-ux.md dochodzi w T14. */
.ft-projects-content { max-width: 1120px; margin: 0 auto; width: 100%; }

/* Rzad chipow filtra H10 — caly wyglad (kolor, ksztalt pigulki) niesie
   MudChip przez SelectedColor, ten kontener tylko oddziela rzad od siatki
   ponizej. .ft-projects-chip-archived na chipie archiwum to WYLACZNIE hak
   testowy/semantyczny (WorkScreensTests.ProjectsRoute_RendersArchiveFilterChips)
   — nie niesie wlasnego stylu, MudChip juz maluje stan wybrany/niewybrany. */
.ft-projects-filter { margin-bottom: 4px; }

.ft-projects-grid {
  display: grid;
  /* min(320px, 100%), nie gołe 320px z makiety (P14 Z3, wejścia P10 poz.
     123): sztywne minimum toru to podłoga, pod którą siatka nie zejdzie, więc
     na ekranie 320 px kolumna treści o 288 px i tak dostawała tor 320 px, a
     każda karta wystawała 32 px za krawędź strony (zmierzone na gałęzi
     archiwalnej, sonda v1: dokument 336 > 320, a.ft-projcard i
     .ft-projcard-ghost do right=336). min() pozwala jedynej kolumnie skurczyć
     się do kontenera poniżej 320 px i niczego nie zmienia powyżej — próg
     drugiej kolumny zostaje ten sam. Pomiar po zmianie: probes/overflow.mjs,
     szerokość 320. */
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  gap: 20px;
  margin-top: 24px;
}

.ft-projcard {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--ft-surface); border: var(--ft-border); border-radius: var(--ft-r-lg);
  padding: 20px; text-decoration: none; color: var(--ft-text-1);
  box-shadow: var(--ft-shadow-sm);
  transition: box-shadow 160ms cubic-bezier(.2,.6,.2,1), transform 160ms cubic-bezier(.2,.6,.2,1);
}
.ft-projcard:hover, .ft-projcard:focus-visible {
  box-shadow: var(--ft-shadow-md); transform: translateY(-1px);
}

/* Karta zarchiwizowana (H10, spec P7a §4) — wygaszenie CAŁEGO kafla, tym
   samym pomysłem co .ft-tpl-inactive .ft-row niżej (§28, od H1): jedna
   reguła na hoście wystarcza, ProjectCard.razor ma prostszą strukturę niż
   wiersz Szablonów (jeden <a>, bez osobnych kolumn) i nie wymaga długiej
   listy selektorów potomnych. */
.ft-projcard-archived { opacity: 0.55; }

/* Badge stanu "zarchiwizowany" — hairline/text-3, celowo NIE para
   semantyczna (tint/deep) jak .ft-projcard-badge-warning wyżej: to fakt o
   stanie karty, nie ostrzeżenie (ui-ux.md "DON'T use the semantic scale
   for post statuses" — ta sama zasada per analogię tutaj). */
.ft-projcard-badge-archived {
  background: transparent; color: var(--ft-text-3); border: var(--ft-border);
}

/* Jedyne odstepstwo od "cala karta to czysty link" w makiecie — patrz
   komentarz w ProjectCard.razor przy tym elemencie. */
.ft-projcard-archive { position: absolute; top: 10px; right: 10px; }

.ft-projcard-header { display: flex; align-items: center; gap: 12px; padding-right: 34px; }
.ft-projcard-monogram {
  width: 42px; height: 42px; border-radius: var(--ft-r-md); flex: none;
  display: grid; place-items: center;
  /* Ink stały biały (jak AvatarColorHelper gdzie indziej) — pallet nie
     jest motywowany, więc tekst na nim też nie powinien być. */
  color: var(--ft-avatar-ink); font: 700 14px/1 var(--ft-font);
}
.ft-projcard-headtext { min-width: 0; }
/* Jedna linia + elipsa (nie 2-line clamp): karty w gridzie musza trzymac
   spojna wysokosc naglowka, zeby .ft-skel-projcard nie rozjezdzalo sie z
   realnymi kartami o zroznicowanej dlugosci nazwy. Wymaga min-width:0 na
   .ft-projcard-headtext powyzej (flex item bez tego nie kurczy sie ponizej
   tresci). */
.ft-projcard-name {
  font: var(--ft-type-h3); color: var(--ft-text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ft-projcard-meta { font: var(--ft-type-micro); color: var(--ft-text-3); margin-top: 1px; }

.ft-projcard-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; align-items: center; }
.ft-projcard-badge { font: var(--ft-type-micro); border-radius: var(--ft-r-full); padding: 3px 10px; }
.ft-projcard-badge-pending { background: var(--ft-st-pending-tint); color: var(--ft-st-pending-deep); }
.ft-projcard-badge-planned { background: var(--ft-st-planned-tint); color: var(--ft-st-planned-deep); }
.ft-projcard-badge-warning { background: var(--ft-warning-tint); color: var(--ft-warning-deep); }
.ft-projcard-badge-toggle {
  display: inline-flex; align-items: center; gap: 3px;
  font: var(--ft-type-micro); color: var(--ft-text-3);
  background: var(--ft-surface-2); border: none; border-radius: var(--ft-r-full);
  padding: 3px 9px; cursor: pointer; transition: background 120ms;
}
.ft-projcard-badge-toggle:hover, .ft-projcard-badge-toggle:focus-visible { background: var(--ft-hairline); }
.ft-projcard-badge-toggle svg { transition: transform 120ms; }

.ft-projcard-stats {
  display: flex; gap: 14px; margin-top: 16px; font: var(--ft-type-small); color: var(--ft-text-2);
}
.ft-projcard-stats strong {
  color: var(--ft-text-1); font: var(--ft-type-mono); font-variant-numeric: var(--ft-tabular);
}

.ft-projcard-platforms { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; align-items: center; }
.ft-projcard-platform {
  display: inline-flex; align-items: center; gap: 5px;
  font: var(--ft-type-micro); color: var(--ft-text-2);
  border: var(--ft-border); border-radius: var(--ft-r-full); padding: 3px 10px;
}
.ft-projcard-platform-dot { width: 7px; height: 7px; border-radius: 999px; flex: none; }
.ft-projcard-health-ok   { color: var(--ft-success-deep); }
.ft-projcard-health-warn { color: var(--ft-warning-deep); }
.ft-projcard-health-bad  { color: var(--ft-danger-deep); }

/* --ft-text-3: stopka niesie czytelny tekst („ostatnia aktywność: …"), nie
   dekorację — mapa ze starej nazwy z makiety w tokens.css przy --ft-decor. */
.ft-projcard-footer {
  font: var(--ft-type-mono); font-size: 11px; color: var(--ft-text-3);
  margin-top: auto; padding-top: 16px;
}

.ft-projcard-ghost {
  min-height: 200px; background: transparent; border: 2px dashed var(--ft-hairline);
  border-radius: var(--ft-r-lg); display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px;
  color: var(--ft-text-3); font: 600 14px/20px var(--ft-font); cursor: pointer;
  transition: border-color 120ms, color 120ms; width: 100%;
}
.ft-projcard-ghost:hover, .ft-projcard-ghost:focus-visible {
  border-color: var(--ft-accent); color: var(--ft-accent-text);
}

/* Karta onboardingowa (stan pusty) — bogatsza niż generyczny EmptyState
   (kafel z logo na gradiencie, dodatkowa notka pod CTA), więc bespoke
   markup zamiast <EmptyState> tutaj; spec P5 §5 opisuje ją wprost wg
   makiety Projekty.dc.html. */
.ft-projects-onboard-wrap { margin-top: 40px; display: grid; place-items: center; }
.ft-projects-onboard {
  max-width: 560px; background: var(--ft-surface); border: var(--ft-border); border-radius: var(--ft-r-xl);
  padding: 56px 48px; display: flex; flex-direction: column; align-items: center; gap: 20px;
  text-align: center; box-shadow: var(--ft-shadow-md);
}
.ft-projects-onboard-tile {
  width: 72px; height: 72px; border-radius: 22px;
  background: linear-gradient(135deg, var(--ft-accent) 0%, var(--ft-accent-strong) 100%);
  display: grid; place-items: center; box-shadow: var(--ft-shadow-lg);
}
.ft-projects-onboard-tile img { width: 42px; height: 42px; }
.ft-projects-onboard h2 { font: var(--ft-type-h2); margin: 0; color: var(--ft-text-1); }
.ft-projects-onboard p {
  font: var(--ft-type-body); color: var(--ft-text-3); margin: 10px auto 0; max-width: 42ch; text-wrap: pretty;
}
.ft-projects-onboard-note { font: var(--ft-type-micro); color: var(--ft-text-3); }

/* ---- 24. Kalendarz (P5) -----------------------------------------------------
   Calendar.razor (T10-T11 budowały go OBOK starej tabelarycznej strony
   pod tymczasową nazwą pliku; T12 zrobił cutover — rename + git rm
   starego, patrz komentarz na górze pliku) + CalendarDayCell.razor —
   siatka CSS grid od zera zamiast MudSimpleTable, wg makiety
   mockups/flowtent-design/project/Posty.dc.html (karta kalendarza
   L145-193, spec P5 §6). Klasa siatki celowo BEZ myślnika —
   "ft-calgrid", nie "ft-cal-grid": dokładny string, którego szuka fail-first
   fakt CalendarRoute_RendersGridWithStatusChipsAndKeyboardLegend
   (WorkScreensTests, napisany w T10, odskipowany w T12). Wszystkie pozostałe
   klasy sekcji trzymają myślnikowaną rodzinę "ft-cal-*".

   Szarość, którą makieta nazywa własnym tokenem, mapuje się u nas na
   --ft-text-3 (mapa w tokens.css przy --ft-decor) — dni spoza miesiąca
   i nazwy dni weekendu schodzą tam, ten sam most co Projects.

   Obramowania komórek: border-right/border-bottom na KAŻDEJ komórce (jak w
   makiecie). CalendarDayCell renderuje dokładnie jeden
   <div class="ft-cal-day"> jako korzeń (bez dodatkowego wrappera), więc
   selektory poniżej liczą się poprawnie mimo że każda komórka to osobny
   komponent.

   Realna siatka (rola "grid"): tygodnie są opakowane w <div role="row"
   style="display:contents"> (F1 przeglądu T10, wymóg modelu ARIA grid —
   patrz komentarz w Calendar.razor przy pętli tygodni). display:contents
   NIE tworzy własnego boxu, więc komórki nadal są bezpośrednimi grid-items
   .ft-calgrid — wizualnie nic się nie zmienia. Zmienia się za to drzewo
   DOM, na którym liczą :nth-*: .ft-cal-day:nth-child(7n) nadal trafia w
   ostatnią kolumnę KAŻDEGO wiersza (parent to teraz row-div z dokładnie 7
   dziećmi, więc pozycja 7 = ostatnia = ta sama komórka co przed zmianą),
   ale :nth-last-child(-n+7) licząca "ostatnie 7 z płaskiej listy" już NIE
   działa — w wierszu o 7 dzieciach dopasowałaby WSZYSTKIE 7 (zdjęłaby
   border-bottom z każdego wiersza, nie tylko ostatniego). Stąd osobny,
   jawnie wiersz-owy selektor niżej: .ft-calgrid > [role="row"]:last-child
   .ft-cal-day — DOM parent/child nie zależy od display:contents, więc to
   działa niezależnie od tego, czy siatka ma 5 czy 6 tygodni (max 6, spec).

   Szkielet ładowania NIE ma opakowania row (role="status", nie role="grid"
   — bez wymogu ARIA row/gridcell), więc jego komórki zostają płaskimi
   dziećmi .ft-calgrid.ft-cal-skel — .ft-cal-skel-cell:nth-last-child(-n+7)
   niżej liczy się tak jak przed F1, bez zmian. */

.ft-cal-header {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 20px; border-bottom: var(--ft-border);
}
.ft-cal-monthlabel { font: var(--ft-type-h3); margin: 0; }
.ft-cal-today-btn {
  font: var(--ft-type-micro); color: var(--ft-accent-text);
  border: var(--ft-border); background: transparent; border-radius: var(--ft-r-full);
  padding: 5px 12px; cursor: pointer; transition: background 120ms;
}
.ft-cal-today-btn:hover, .ft-cal-today-btn:focus-visible { background: var(--ft-accent-tint); }
.ft-cal-count { margin-left: auto; font: var(--ft-type-small); color: var(--ft-text-3); }

/* Nagłówek kalendarza na wąskim ekranie (P9 §5 poz. 7). .ft-cal-header to flex
   BEZ flex-wrap z pięciorgiem dzieci (2× stepper, nazwa miesiąca, „Dziś",
   licznik z margin-left:auto), więc przy 390 px nazwa miesiąca i licznik nie
   mieszczą się obok siebie i „wrzesień 2026" łamie się na dwie linie
   (zmierzone P8/T12). Poniżej 760 px wiersz zawija, a licznik — jako jedyna
   DANA w nagłówku, reszta to nawigacja — schodzi na własną, pełną linię
   (flex-basis:100% + zdjęte margin-left:auto, które inaczej dokładałoby mu
   pusty odstęp na starcie linii). Padding poziomy 20 → 16 px, tyle co reszta
   ekranów w ich blokach 760 px (Profil, Członkowie). Przy Loading licznik nie
   jest rysowany (jak dziś), więc nagłówek jest o linię niższy — akceptowane,
   bo szkielet siatki i tak zmienia wysokość strony. */
@media (max-width: 760px) {
  .ft-cal-header { flex-wrap: wrap; padding: 12px 16px; }
  .ft-cal-count { flex-basis: 100%; margin-left: 0; }
}

.ft-cal-daynames {
  display: grid; grid-template-columns: repeat(7, 1fr);
  border-bottom: var(--ft-border); font: var(--ft-type-micro); color: var(--ft-text-3);
}
.ft-cal-dayname { padding: 8px 10px; }
.ft-cal-dayname-weekend { color: var(--ft-text-3); opacity: 0.75; }

.ft-calgrid {
  display: grid; grid-template-columns: repeat(7, 1fr); grid-auto-rows: minmax(104px, auto);
}
.ft-cal-day {
  border-right: var(--ft-border); border-bottom: var(--ft-border);
  padding: 8px; display: flex; flex-direction: column; gap: 4px;
  cursor: pointer;
}
.ft-cal-day:nth-child(7n) { border-right: none; }
/* Row-scoped, NIE :nth-last-child(-n+7) na .ft-cal-day — patrz wyjaśnienie
   display:contents w komentarzu sekcji wyżej (F1 przeglądu T10). */
.ft-calgrid > [role="row"]:last-child .ft-cal-day { border-bottom: none; }
.ft-cal-day:focus-visible { outline: 2px solid var(--ft-accent); outline-offset: -2px; }
.ft-cal-day.ft-cal-weekend { background: var(--ft-surface-2); }
.ft-cal-day.ft-cal-out .ft-cal-daynum { color: var(--ft-text-3); }
/* [nowy, P8/T5 — poprawka po przeglądzie] Wyciszenie komórki spoza miesiąca
   obejmuje TAKŻE jej chipy. Do P8 nie musiało: BuildCalendar zerował posty
   takich dni, więc komórka niosła sam numer. Od T5 host ładuje zakres całej
   siatki i dzień-wypełnienie rysuje pełnoprawne chipy — w siatce października
   ostatnią komórką jest 1 listopada, czyli wyciszony numer „1" obok chipa w
   pełnym kolorze statusu.

   Wyciszamy WYPEŁNIENIEM, nie `opacity` — zmierzone przy pisaniu tej reguły
   (ui-ux.md: „If you invent a combination the file does not name, measure it
   before shipping"). Chip to para tint/deep z tokens.css, o kontraście
   5,2–7,0:1 w motywie jasnym; `opacity` miesza OBA kolory z tłem komórki, więc
   przy .72 najsłabsza para (published) spada do 3,05:1, a przy .75 do 3,22:1 —
   poniżej AA 4,5:1 dla tekstu, którego chip jest w dodatku linkiem (role="link",
   Enter otwiera detal). Alpha utrzymująca AA to dopiero ~.95, czyli wyciszenie
   niewidoczne. Zdjęcie samego tła zostawia deep na powierzchni komórki, gdzie
   kontrast jest NIE MNIEJSZY niż na tincie (tint leży między powierzchnią a
   deep w obu motywach) — i jest tym samym idiomem, którym siatka wycisza numer
   dnia linijkę wyżej: zmianą koloru, nie przezroczystością. */
.ft-cal-day.ft-cal-out .ft-cal-chip { background: transparent; }
/* Dziś: obrys 2px --ft-accent, offset -2 (spec P5 §6) — nadpisuje
   :focus-visible tylko wizualnie tym samym traktowaniem, więc komórka
   dzisiejsza fokusowana wygląda identycznie jak dzisiejsza niefokusowana,
   co jest zamierzone (nie dwa nakładające się obrysy). */
.ft-cal-day.ft-cal-today {
  outline: 2px solid var(--ft-accent); outline-offset: -2px; border-radius: 2px;
}
.ft-cal-daynum { font: var(--ft-type-micro); color: var(--ft-text-2); }
.ft-cal-day.ft-cal-today .ft-cal-daynum { color: var(--ft-accent-text); font-weight: 600; }

.ft-cal-chip {
  position: relative; display: flex; align-items: center; gap: 5px;
  border-radius: var(--ft-r-sm); padding: 3px 7px; font: var(--ft-type-micro);
  white-space: nowrap; overflow: hidden; cursor: pointer;
}
.ft-cal-chip-time { font-variant-numeric: var(--ft-tabular); flex: 0 0 auto; }
.ft-cal-chip-topic { overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
.ft-cal-chip-failicon { flex: 0 0 auto; }
/* Odznaka nieprzeczytanych — świadome wzbogacenie wzgledem makiety (spec P5
   §6: "Badge nieprzeczytanych zostaje"), w rogu chipa jak stary MudBadge
   Overlap, tym razem czystym CSS zamiast komponentu MudBlazor (chip sam
   przestał być MudChip). */
.ft-cal-chip-unread {
  position: absolute; top: -5px; right: -5px;
  min-width: 15px; height: 15px; border-radius: 999px; background: var(--ft-accent-strong);
  color: var(--ft-on-accent); font: 600 9px/15px var(--ft-font); text-align: center; padding: 0 3px;
}

.ft-cal-legend {
  padding: 10px 20px; border-top: var(--ft-border); font: var(--ft-type-micro); color: var(--ft-text-3);
}

/* Szkielet ładowania (7×5 komórek, 1-2 migoczące chipy — spec P5 §6). Ta
   sama obramówka co realne komórki, żeby siatka nie "skoczyła" gdy dane
   podmienią szkielet (ui-ux.md "DO size the skeleton to the height of the
   content it replaces"). */
.ft-cal-skel-cell {
  border-right: var(--ft-border); border-bottom: var(--ft-border);
  min-height: 104px; padding: 8px; display: flex; align-items: flex-start;
}
.ft-cal-skel-cell:nth-child(7n) { border-right: none; }
.ft-cal-skel-cell:nth-last-child(-n+7) { border-bottom: none; }
.ft-cal-skel-chip { width: 70%; height: 18px; border-radius: var(--ft-r-sm); }

/* Wiersz agendy (P5/T11 jako dialog dnia, WYDZIELONY do CalendarAgendaRow
   .razor w T12) — Enter na komórce z >1 postem otwiera dialog; agenda
   mobile (T12, poniżej) używa DOKŁADNIE tego samego znacznika (jeden
   komponent, dwa kontenery-rodzice: .ft-cal-agenda-rows w dialogu i po
   jednym per grupa dnia w agendzie). Anatomia wiersza: kafelek 40px w
   tincie statusu (.ft-status-* modyfikatory z sekcji 6 dają samo
   tło+kolor, tu tylko rozmiar/kształt), tytuł 600 z ellipsis, meta
   kropka brandu + platforma/format/godzina, glif statusu na końcu. Cały
   wiersz to <a href> (prawdziwy link, nie OnClick) — spójne z resztą
   aplikacji (UpcomingList.razor, ContentPostRow.razor). Separator
   (border-top, pierwszy dziecko bez) jest SCOPED do bezpośredniego
   rodzica .ft-cal-agenda-rows — każda grupa dnia w agendzie mobile ma
   własny kontener, więc pierwszy wiersz KAŻDEJ grupy jest bez górnej
   kreski, nie tylko pierwszy wiersz całej strony. */
.ft-cal-agenda-rows { display: flex; flex-direction: column; }
.ft-cal-agrow {
  display: flex; align-items: center; gap: 10px; padding: 8px 0;
  text-decoration: none; color: inherit; border-top: var(--ft-border);
}
.ft-cal-agenda-rows > .ft-cal-agrow:first-child { border-top: none; }
.ft-cal-agrow-tile { width: 40px; height: 40px; border-radius: var(--ft-r-sm); flex: 0 0 auto; }
.ft-cal-agrow-main { min-width: 0; flex: 1 1 auto; }
.ft-cal-agrow-topic {
  display: block; font: var(--ft-type-body); font-weight: 600; color: var(--ft-text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ft-cal-agrow-meta { display: flex; align-items: center; gap: 6px; font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-cal-agrow-dot { width: 7px; height: 7px; border-radius: 999px; flex: 0 0 auto; }
.ft-cal-agrow-status { flex: 0 0 auto; color: var(--ft-text-2); display: flex; }

/* Agenda mobile (P5/T12, spec P5 §6 "Mobile (< 760px): agenda miesiąca")
   — grupy dni Z POSTAMI (puste dni pominięte), nagłówek dnia + wiersze
   .ft-cal-agenda-rows powyżej. Odstęp między grupami przez margin, nie
   gap na .ft-cal-agenda, żeby nie trzeba było owijać ostatniej grupy w
   warunek "nie ostatnia". Poziomy odstęp 20px (spójny z .ft-cal-header/
   .ft-cal-legend) siedzi na .ft-cal-agenda-group, NIE na
   .ft-cal-agenda-rows — ten drugi selektor jest WSPÓLNY z dialogiem dnia
   (CalendarDayDialog owija nim wiersze bez opakowania w grupę), więc
   padding tam wyciekłby do wnętrza dialogu, gdzie MudBlazor już daje
   własny odstęp treści. */
.ft-cal-agenda-group { margin-bottom: 16px; padding: 0 20px; }
.ft-cal-agenda-group:last-child { margin-bottom: 0; }
.ft-cal-agenda-daylabel {
  font: var(--ft-type-small); font-weight: 600; color: var(--ft-text-2);
  margin: 0 0 4px;
}

/* Cichy komunikat agendy (P9 §6 poz. 9) — miesiąc, którego jedyne posty leżą w
   dniach-wypełnienia siatki, na telefonie nie ma ŻADNEJ grupy dnia, a pusty stan
   z ilustracją i CTA stoi na CAŁEJ siatce (Posts.Count == 0), więc się tu nie
   pokaże. Ton notki, nie alertu: --ft-text-3 jak każda notka w aplikacji. Odstęp
   poziomy 20 px jak .ft-cal-agenda-group wyżej — komunikat staje w tej samej
   kolumnie co nagłówki dni, które zastępuje (spec §6 pisała „12px 0"; zerowy
   odstęp dosunąłby go do krawędzi karty, bo .ft-cal-agenda sama paddingu nie ma). */
.ft-cal-agenda-empty {
  font: var(--ft-type-small); color: var(--ft-text-3);
  margin: 0; padding: 12px 20px;
}

/* Szkielet agendy mobile (P5/12 review F2). Poprawka: `.ft-calgrid:not(
   .ft-cal-skel)` wcześniej trzymało 7-kolumnową siatkę widoczną (ściśniętą)
   pod 760px w trakcie ładowania, po czym dane zamieniały ją w jednokolumnową
   agendę — dokładnie skok kształtu, którego zakazuje ui-ux.md "DO size the
   skeleton to the height of the content it replaces". Teraz `.ft-calgrid`
   (WŁĄCZNIE ze szkieletem) gaśnie <760px jak realna siatka, a ten blok daje
   agendzie WŁASNY szkielet o jej własnych proporcjach: kafelek 40px +
   dwa paski tekstu, jak `.ft-cal-agrow`/`CalendarAgendaRow` — 5 wierszy
   (`AgendaSkeletonRows` w Calendar.razor), nie licznik dni miesiąca (agenda
   nie ma "tygodni" do podpięcia się pod, w przeciwieństwie do siatki). */
.ft-cal-agenda-skel { display: none; }
.ft-cal-agrow-skel {
  display: flex; align-items: center; gap: 10px; padding: 8px 20px;
  border-top: var(--ft-border);
}
.ft-cal-agenda-skel > .ft-cal-agrow-skel:first-child { border-top: none; }
.ft-cal-agrow-skel-tile { width: 40px; height: 40px; border-radius: var(--ft-r-sm); flex: 0 0 auto; }
.ft-cal-agrow-skel-main { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; }
.ft-cal-agrow-skel-topic { height: 14px; width: 60%; border-radius: var(--ft-r-sm); }
.ft-cal-agrow-skel-meta { height: 10px; width: 40%; border-radius: var(--ft-r-sm); }

/* Przełącznik siatka/agenda po breakpoincie CSS (BEZ JS-owego wykrywania
   viewportu — spec P5 §6, decyzja planu T12). Obie struktury (dane I
   szkielety) żyją w markupie zawsze (prerender niesie wszystkie — koszt
   renderu miesiąca postów jest znikomy); to display: none/block przełącza,
   która jest widoczna. Domyślnie (bez media query) = desktop: siatka
   widoczna, agenda i jej szkielet ukryte — reguła max-width poniżej
   odwraca to na wąskim viewporcie, zamiast pisać osobny min-width blok dla
   "≥760px" (ten sam styl co reszta pliku: konsola zgód, sekcja 22 — jedna
   reguła max-width, cascade robi resztę). `.ft-calgrid` bez wyjątku dla
   `.ft-cal-skel` (poprawka F2 — patrz komentarz nad `.ft-cal-agenda-skel`
   wyżej): siatka i JEJ szkielet znikają razem <760px, agenda i JEJ
   szkielet pokazują się razem — żadna z dwóch faz (ładowanie/dane) nie
   zmienia kształtu przy przejściu przez breakpoint. */
.ft-cal-agenda { display: none; padding-top: 4px; }
@media (max-width: 760px) {
  .ft-cal-daynames,
  .ft-calgrid,
  .ft-cal-legend {
    display: none;
  }
  .ft-cal-agenda { display: block; }
  .ft-cal-agenda-skel { display: flex; flex-direction: column; padding-top: 4px; }
}

/* ---- 25. Strony systemowe (P6) --------------------------------------------
   404 (PageNotFound) i Błąd serwera (Error) — makieta Strony Systemowe.dc.html
   (L25-49, spec P6 §4, wejście P6 nr 7). Wzorzec strony prawnej z tej samej
   makiety (L51-78) świadomie POZA zakresem — dokumenty prawne żyją na apexie.

   Jedna karta wyśrodkowana w dostępnej wysokości .ft-shell-body — BEZ
   własnego min-height:100vh jak w makiecie (tam to samodzielny dokument HTML;
   tutaj .ft-shell-main (§19) już jest kolumną flex min-height:100vh, a
   .ft-shell-body jest jej flex:1 dzieckiem, więc ma realną, niezerową
   wysokość mimo że sam nie jest flex-containerem — height:100% na
   .ft-syspage się do niej odwołuje. Podwójne 100vh dublowałoby rezerwę na
   pasek 52px, którą .ft-shell-main już odjęła w paddingu.

   Kod błędu, tytuł, opis i mono-linia dzielone przez obie strony przez
   wspólne klasy .ft-syspage-*. Krąg danger (.ft-syspage-badge) tylko w
   Error — większy odpowiednik .ft-state-badge z §11 (64px zamiast 38px:
   pełnostronicowa granica systemu, nie inline retry w środku karty jak
   ErrorState.razor), ta sama ścieżka SVG dla spójnej ikonografii błędu w
   całej aplikacji. */
.ft-syspage {
  display: grid;
  place-items: center;
  height: 100%;
  padding: 32px;
}
.ft-syspage-card {
  display: flex; flex-direction: column; align-items: center; gap: 20px;
  text-align: center; max-width: 480px;
  animation: ft-fade-up var(--ft-dur-base) var(--ft-ease-out);
}
.ft-syspage-illus { flex: 0 0 auto; }
.ft-syspage-code {
  font: 700 60px/64px var(--ft-font); letter-spacing: -0.02em;
  color: var(--ft-text-3); font-variant-numeric: var(--ft-tabular);
}
.ft-syspage-badge {
  display: grid; place-items: center; flex: 0 0 auto;
  width: 64px; height: 64px; border-radius: var(--ft-r-full);
  background: var(--ft-danger-tint); color: var(--ft-danger-deep);
}
.ft-syspage-title { font: var(--ft-type-h1); letter-spacing: -0.01em; margin: 0; color: var(--ft-text-1); }
.ft-syspage-desc {
  font: var(--ft-type-body); color: var(--ft-text-3);
  margin: 8px auto 0; max-width: 44ch; text-wrap: pretty;
}
.ft-syspage-ctas { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
/* max-width + overflow-wrap: .ft-syspage-card jest flex column align-items:
   center — dzieci NIE są przycinane do jej 480px, a _requestedPath (404) to
   nieograniczony tekst bez naturalnych punktów łamania. Na 375px telefonu
   (~311px użytecznej szerokości przy padding:32px z .ft-syspage) długa
   ścieżka bez tej reguły wypycha poziomy scroll całej strony — zmierzone w
   przeglądzie jakości T7. */
.ft-syspage-mono {
  font: var(--ft-type-mono); font-size: 11px; color: var(--ft-text-3);
  max-width: 100%; overflow-wrap: break-word;
}

/* ---- 26. Profil (P6/T8) ---------------------------------------------------
   Cutover w miejscu (`/profile`) — makieta Profil.dc.html (L38-99, spec P6
   §5). Cap 760px — DRUGA nazwana wyjątkowa szerokość po 1120px Projektów
   (ui-ux.md "Page Layout"; notka pochodna do ui-ux.md przyjdzie w T14) —
   ten sam wzorzec co .ft-projects-content §23: shell capuje już na 1600px,
   to jest świadomy DRUGI cap, nie omyłkowe podwójne zawinięcie.

   Trzy karty (Dane konta / Powiadomienia / Bezpieczeństwo) dzielą jeden
   kształt "head / body / foot" zamiast płaskiego MudPaper pa-4 z ui-ux.md
   "Cards & Surfaces" — makieta rysuje pasek nagłówka z border-bottom i
   stopkę-akcję z border-top na KAŻDEJ z trzech kart (Zapisz / -- /
   Zmień hasło), więc jeden wspólny wzorzec head/body/foot, nie trzy osobne
   układy. */
.ft-profile { max-width: 760px; margin: 0 auto; width: 100%; }
.ft-profile-card { overflow: hidden; }
.ft-profile-card-head {
  padding: 14px 24px; border-bottom: 1px solid var(--ft-hairline);
}
.ft-profile-card-body { padding: 20px 24px; }
.ft-profile-card-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 24px; border-top: 1px solid var(--ft-hairline);
}
.ft-profile-foot-note { font: var(--ft-type-micro); color: var(--ft-text-3); }

/* Helper-text pól Mud w roli NOTKI (P9 §5 poz. 8). Zmierzone w Profilu: dwa
   `HelperText=` (Profile.razor:69, 266) renderują się jako
   .mud-input-helper-text w --mud-palette-text-secondary (#46545F rgb(70,84,95)
   = --ft-text-2), a KAŻDA własna notka tego ekranu ma --ft-text-3
   (rgb(95,108,120)) — dwie szarości w tej samej roli obok siebie. Notka to
   „metadata" z tabeli ui-ux.md, więc wygrywa --ft-text-3 (5.38:1 na surface,
   5.8:1 w dark — tokenem, czyli w obu motywach). Reguła TU, nie w palecie
   MudBlazor: #46545F to jednocześnie Secondary, DrawerIcon i ActionDefault,
   więc zmiana w FlowtentTheme.cs ruszyłaby ikony szuflady i domyślny kolor
   akcji. Selektor jest ogólny, więc skutek jest APLIKACYJNY, nie profilowy:
   każdy helper-text ciemnieje o jeden stopień — to ujednolicenie, nie
   regresja (spec §5 poz. 8). Siedzi w sekcji Profilu, bo tam kolizję
   zmierzono. */
.mud-input-helper-text { color: var(--ft-text-3); }

/* Karta "Dane konta": awatar 72px (kolumna wyśrodkowana) + siatka pól 2-kol
   (imię, nazwisko, e-mail readonly, slug, język, motyw — spec §5 "imię i
   nazwisko edytowalne" implikuje DWA pola, nie jedno złożone "Imię i
   nazwisko" jak w toy-state makiety: prawdziwy UpdateProfileCommand
   przyjmuje FirstName/LastName osobno, sklejanie-i-rozdzielanie pełnego
   imienia jest kruche na złożonych nazwiskach). Slug w makiecie NIE
   występuje, ale UpdateProfileCommand go wymaga i istniejący dialog go
   edytował — zostaje w siatce jako rozszerzenie ponad makietę (ten sam
   precedens co ChangeProjectSlug w spec §8 "Ustawienia"), nie cichy regres
   funkcji. */
.ft-profile-account-body { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
.ft-profile-avatar-col { display: flex; flex-direction: column; align-items: center; gap: 8px; flex: 0 0 auto; }
.ft-profile-avatar-hint {
  font-size: 11px; color: var(--ft-text-3); text-align: center; max-width: 96px;
}
.ft-profile-fields { flex: 1; min-width: 260px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
.ft-profile-field { display: flex; flex-direction: column; gap: 5px; }
.ft-profile-field-label { font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-profile-email {
  display: block; border: 1px solid var(--ft-hairline); border-radius: var(--ft-r-md);
  padding: 10px 13px; font: var(--ft-type-mono); color: var(--ft-text-2);
  background: var(--ft-surface-2);
}
@media (max-width: 760px) {
  .ft-profile-fields { grid-template-columns: 1fr; }
}

/* Segmentowany przełącznik — język (PL/EN, mechanizm culture-cookie
   niezmieniony) i motyw (Systemowy/Jasny/Ciemny, kaskada ShellTheme z
   ShellLayout — patrz Theme/ShellThemeState.cs, DLACZEGO nie druga
   instancja ThemeSync). Pierwszy call site tego wzorca w aplikacji —
   role="group" + aria-checked na każdym segmencie, ten sam kontrakt co
   menuitemradio ShellTopBar (spójna semantyka, nie identyczny markup: tu
   nie ma menu do zamknięcia). */
.ft-segmented {
  display: inline-flex; border: 1px solid var(--ft-hairline);
  border-radius: var(--ft-r-md); overflow: hidden; width: fit-content;
}
.ft-segmented-item {
  padding: 8px 14px; font: var(--ft-type-micro); color: var(--ft-text-2);
  background: transparent; border: none; border-left: 1px solid var(--ft-hairline);
  cursor: pointer; font-family: inherit;
}
.ft-segmented-item:first-child { border-left: none; }
.ft-segmented-item.active { background: var(--ft-accent-tint); color: var(--ft-accent-text); }
/* Segmenty motywu są disabled do chwili, gdy kaskada ShellTheme zgłosi
   Available (przegląd T8, poprawka 2) — żaden segment nie pokazuje
   podświetlenia w tym oknie, więc samo disabled potrzebuje własnego
   sygnału, inaczej wygląda jak martwy przycisk bez wyjaśnienia. *@
.ft-segmented-item:disabled { cursor: not-allowed; opacity: 0.55; }

/* Karta "Powiadomienia": wiersz push (urządzenie) + jeden wiersz na
   NotificationType (7 — pełny inwentarz z modelu SetNotificationPreference,
   NIE tylko podzbiór z makiety) + jeden disabled-z-tooltipem wiersz
   "podsumowanie tygodnia" (typ nie istnieje w NotificationType — reguła
   ui-ux.md "planowane funkcje widoczne"). Separator między wierszami to
   inline <div style="border-top..."> (wzorzec ui-ux.md "Lists & Threads" /
   już obecny w starym kodzie tej strony), nie osobna klasa — spójnie z
   resztą aplikacji. */
.ft-profile-row { display: flex; align-items: center; gap: 14px; padding: 13px 0; flex-wrap: wrap; }
.ft-profile-row-text { flex: 1; min-width: 200px; }
.ft-profile-row-title { font: 600 14px/20px var(--ft-font); color: var(--ft-text-1); }
.ft-profile-row-desc { font: var(--ft-type-small); color: var(--ft-text-3); }
.ft-profile-channels { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.ft-profile-channel { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.ft-profile-channel-label {
  font-size: 10px; letter-spacing: 0.02em; text-transform: uppercase; color: var(--ft-text-3);
}

/* Przełącznik pigułkowy (role="switch") — pierwszy call site w aplikacji;
   grep "role=\"switch\"" był pusty przed tym zadaniem. Gałka #fff na stałe
   (nie token): ten sam udokumentowany wyjątek markup-color co ink awatarów
   (spec §0, komentarz przy .ft-avatar) — siedzi na stałym torze
   accent-strong/decor, nie na motywowanym tle.

   Tor wyłączony to --ft-decor, a NIE --ft-text-3 jak reszta przemiecenia
   z P8: tor niczego nie pisze, jest kształtem pod gałką, czyli dokładnie
   rolą tokenu dekoracji. Do P8 stała tu nazwa spoza słownika (mapa w
   tokens.css przy --ft-decor), a `background` z nieistniejącą zmienną nie
   dziedziczy — degeneruje się do wartości początkowej, czyli `transparent`:
   wyłączony przełącznik nie miał toru wcale, tylko białą gałkę na białej
   karcie.

   `display: inline-block` jest tu WARUNKIEM POPRAWNOŚCI, nie kosmetyką
   (przegląd zrzutów P8). Profil renderuje ten sam przełącznik w dwóch
   postaciach: aktywną jako <button class="ft-switch">, a nieaktywną jako
   <span class="ft-switch ft-switch-disabled" role="switch"> — trzy stany,
   których nie da się przełączyć (push odmówiony, push niewspierany,
   podsumowanie tygodnia bez typu w NotificationType). <button> jest u agenta
   użytkownika inline-block, więc bierze te 34x19; <span> jest `inline`, a na
   pudełku liniowym `width`/`height` NIE DZIAŁAJĄ w ogóle — sonda DOM
   zmierzyła te spany jako 0x0: w jasnym motywie w miejscu kontrolki same
   białe piksele, w ciemnym goła gałka bez toru. `flex: 0 0 auto` w tej samej
   regule ratuje wyłącznie te call site'y, których rodzic JEST kontenerem
   flex (przyciski kanałów w .ft-profile-channel i .ft-profile-row); każdy
   wyłączony <span> siedzi w środku własnego <div> MudTooltipa, który flexem
   nie jest. Blokifikacja we flexie zjada `inline-block` bez śladu, więc dla
   pozostałych call site'ów ta linia niczego nie zmienia. */
.ft-switch {
  display: inline-block;
  width: 34px; height: 19px; border-radius: var(--ft-r-full);
  background: var(--ft-decor); position: relative; flex: 0 0 auto;
  border: none; padding: 0; cursor: pointer; transition: background 160ms;
}
.ft-switch-on { background: var(--ft-accent-strong); }
.ft-switch-disabled { cursor: not-allowed; opacity: 0.55; }
.ft-switch-knob {
  position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
  border-radius: var(--ft-r-full); background: #fff;
  transition: transform 160ms cubic-bezier(.2,.6,.2,1);
}
.ft-switch-on .ft-switch-knob { transform: translateX(15px); }

/* ---- 27. Dziennik zmian (P6/T9) --------------------------------------------
   Cutover w miejscu (`/audit`) — makieta „Dziennik Zmian.dc.html" (L52-122,
   spec P6 §6). Grupy dniowe jako karty osobne (nie jedna płaska konsola jak
   w starej `.audit-log-console`), rozdzielone nagłówkiem dnia POZA kartą —
   dokładnie układ makiety (L70-71, L98-99): etykieta dnia, potem karta z
   wierszami rozdzielonymi hairline (wzorzec ui-ux.md „Lists & Threads").

   .ft-avatar-26 — TRZECI rozmiar poza spec'owym 24/32/40 (po .ft-avatar-72
   z Profilu §26), świadome rozszerzenie tej samej rodziny .ft-avatar, nie
   nowy dialekt: makieta rysuje awatar wiersza dziennika dokładnie na 26px
   (L74, L80, L86, L102 itd.), między stosem 24px a gniazdem głównym 32px
   z ui-ux.md „Avatars". */
.ft-audit { margin-top: 4px; }
.ft-audit-daylabel {
  font: var(--ft-type-micro); color: var(--ft-text-3);
  margin: 16px 0 6px;
}
.ft-audit-daylabel:first-child { margin-top: 0; }
.ft-audit-daycard {
  background: var(--ft-surface); border: 1px solid var(--ft-hairline);
  border-radius: var(--ft-r-lg); overflow: hidden;
}
.ft-audit-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 20px;
}
/* Podpowiedź pod „Starszymi wpisami" w gałęzi PUSTEGO wyniku szukajki
   (P8 §6 poz. 5) — mówi, dlaczego przycisk stoi pod komunikatem „brak
   pasujących wpisów". Wyśrodkowana jak przycisk nad nią; metadana, więc
   --ft-text-3. */
.ft-audit-loadolder-hint {
  font: var(--ft-type-micro); color: var(--ft-text-3);
  text-align: center; margin-top: 6px;
}
/* Godzina wiersza w kulturze czytającego (P13/Z12). IBM Plex Mono ma stałą
   szerokość 0.6em, czyli ok. 6.6px przy 11px: pl `07:30` to ok. 33px, en
   `12:45 PM` ok. 53px. Stałe width: 42px łamało angielską godzinę na dwie
   linie, więc min-width + nowrap: polski wiersz zostaje co do piksela (42px),
   angielski się nie łamie. Cena: w en początek zdania przesuwa się o 1ch
   między `7:30 AM` a `12:45 PM`. --ft-tabular nadal PO skrócie font (tokens.css). */
.ft-audit-time {
  font: var(--ft-type-mono); font-size: 11px; color: var(--ft-text-3);
  min-width: 42px; white-space: nowrap; flex-shrink: 0; font-variant-numeric: var(--ft-tabular);
}
.ft-avatar-26 { width: 26px; height: 26px; font-size: 10px; }
/* System (autor bez uwierzytelnionego HttpContext — od P7c/T5 realna ścieżka:
   worker publikacji i materializator szablonów piszą wpisy jako System,
   patrz AuditInterceptor). Ikona zamiast inicjałów,
   tło --ft-surface-2 zamiast palety AvatarColorHelper — nie jest to "czyjś"
   kolor, to brak autora-człowieka. */
.ft-audit-sys { background: var(--ft-surface-2); color: var(--ft-text-3); }
.ft-audit-sentence {
  font: var(--ft-type-small); color: var(--ft-text-2); min-width: 0; flex: 1 1 auto;
}
.ft-audit-sentence strong { color: var(--ft-text-1); }
/* Pigułka kategorii — rodzina mieszana (spec P6 §6): post/workflow biorą z
   rampy statusów (progress/published, ta sama co StatusChip), członkowie/
   połączenia z pary semantycznej (info/warning) — NIE jest to piąty człon
   skali statusowej ani nowa skala semantyczna, tylko cztery gotowe pary
   tokenów przypisane wg makiety (L76, L94, L104, L110, L116). */
.ft-audit-pill {
  font: var(--ft-type-micro); border-radius: var(--ft-r-full);
  padding: 2px 9px; flex-shrink: 0; margin-left: auto;
}
.ft-audit-cat-posts { color: var(--ft-st-progress-deep); background: var(--ft-st-progress-tint); }
.ft-audit-cat-workflow { color: var(--ft-st-published-deep); background: var(--ft-st-published-tint); }
.ft-audit-cat-members { color: var(--ft-info-deep); background: var(--ft-info-tint); }
.ft-audit-cat-connections { color: var(--ft-warning-deep); background: var(--ft-warning-tint); }

/* .ft-skel-auditrow (przegląd jakości T9, punkt 4) — .ft-skel-row (§6a, 57px)
   jest wysokością ListRow, nie .ft-audit-row: dodane na papier z reguł tej
   sekcji (ten sam sposób co .ft-skel-projcard §23, nie
   getBoundingClientRect() jak .ft-skel-kpi) — 26px awatar (.ft-avatar-26,
   najwyższe dziecko przy align-items:center — zdanie na --ft-type-small to
   20px linii, pigułka na --ft-type-micro to 16px) + 2×10px pionowy padding
   .ft-audit-row = 46px. Promień --ft-r-md (nie --ft-r-lg z .ft-audit-daycard):
   każdy pasek szkieletu stoi osobno podczas ładowania (wzorzec
   ContentPosts.razor — bez opakowania w kartę), nie jest jeszcze
   pogrupowany w jedną kartę dnia. */
.ft-skel-auditrow { height: 46px; border-radius: var(--ft-r-md); }

/* ---- 28. Szablony (P6/T10, H1 domknięte w P7a) -----------------------------
   Układ wiersza niesie ListRow (§18) — ten sam komponent co Posty i Pomysły,
   trzecia i ostatnia z trzech list makiety IA na jednym markupie na każdą
   szerokość (H1/H4). Tu zostają WYŁĄCZNIE elementy WŁASNE Szablonów, których
   ListRow nie niesie: wrapper listy, kafel-ikona, pigułka platformy z kropką
   brandu, statystyka użycia mono (.ft-tpl-stat — errata koordynatora P7a §5,
   przeniesiona ze slotu Due do MetaLine, patrz komentarz przy regule niżej),
   badge "nieaktywny" i wygaszenie wiersza nieaktywnego.

   .ft-tpl-dot NIE jest .ft-avatar — to kropka brandu 7px
   (PlatformStyleExtensions.GetBrandColorVar()), ten sam pomysł co project-card
   platform pills (P5/T9), inny rozmiar bo pigułka Szablonów jest tekstowa
   ("Platforma · Format"), nie samym inicjałem.

   Klikalność wiersza (Clickable="false" w PostTemplatesList.razor) i
   responsywność (zwijka 1280px/600px) są teraz kontraktem ListRow, nie tego
   pliku — hover/cursor i grid wiersza usunięte stąd razem ze starym
   .ft-tpl-row. */
.ft-tpl { margin-top: 4px; }
.ft-tpl-footnote {
  font: var(--ft-type-small); color: var(--ft-text-3); margin-top: 12px;
}
.ft-tpl-list {
  background: var(--ft-surface); border: var(--ft-border);
  border-radius: var(--ft-r-lg); overflow: hidden;
}
.ft-tpl-tile {
  display: grid; place-items: center; width: 38px; height: 38px; flex-shrink: 0;
  border-radius: var(--ft-r-md); background: var(--ft-accent-tint); color: var(--ft-accent-text);
}
.ft-tpl-pill {
  display: inline-flex; align-items: center; gap: 5px;
  font: var(--ft-type-micro); color: var(--ft-text-2); flex-shrink: 0;
}
.ft-tpl-dot { width: 7px; height: 7px; border-radius: var(--ft-r-full); flex-shrink: 0; }
/* Statystyka użycia (Templates_UsageStat/_None) — errata koordynatora P7a §5:
   pierwotne mapowanie H1 wsadziło ją w slot Due (118px, --ft-type-small), ale
   "zaplanowano N× · do DD.MM" (24-30 znaków) zawijał się tam na dwie linie i
   łamał rytm 57px wiersza (ContentPostRow.razor L90-93 dokumentuje ten sam
   pomiar dla krótszego stringa). Statystyka wraca do MetaLine, PO pigułce
   platformy — mono, jak wymagał spec §5 — a slot Due zostaje celowo
   nierenderowany (patrz PostTemplatesList.razor's RenderRow); efekt uboczny:
   statystyka przestaje znikać na tablecie (<1280px ListRow chowa Due, a
   MetaLine zostaje). */
.ft-tpl-stat { font: var(--ft-type-mono); color: var(--ft-text-3); }
/* Badge "nieaktywny" — hairline/text-3 celowo, NIE skala semantyczna
   (ui-ux.md "DON'T use the semantic scale for post statuses": pauza szablonu
   to fakt o stanie, nie ostrzeżenie o problemie — ta sama zasada, inny
   ekran). */
.ft-tpl-badge-inactive {
  font: var(--ft-type-micro); color: var(--ft-text-3); background: var(--ft-surface-2);
  border: var(--ft-border); border-radius: var(--ft-r-full); padding: 2px 9px; flex-shrink: 0;
}
/* [zmieniony, P7a/H1] Wygaszenie całego wiersza (ListRow nie ma parametru
   klasy — wrapper .ft-tpl-inactive owija <ListRow> w PostTemplatesList.razor's
   RenderRow), NIE tylko wybranych elementów jak przed H1: rozbicie na
   .ft-tpl-tile/.ft-tpl-title/.ft-tpl-desc/.ft-tpl-pill/.ft-tpl-stat osobno
   nie mapuje się już na strukturę ListRow (te klasy — poza tile/pill/dot —
   zniknęły razem ze starym .ft-tpl-row). */
.ft-tpl-inactive .ft-row { opacity: 0.55; }
/* Kontrast kebaba nieaktywnego wiersza (przegląd zgodności, poprawka 4):
   wygaszenie CAŁEGO wiersza regułą wyżej ciągnęło za sobą też menu "…" —
   ikona przy opacity:.55 na --ft-surface spada do ok. 3:1, poniżej WCAG AA.
   Kebab (.ft-row-actions) jest jedynym wejściem do akcji szablonu (P6
   celowo wyłączał .ft-tpl-row z cursor:pointer/hover, patrz komentarz
   wyżej), więc musi zostać w pełni czytelny i klikalny niezależnie od stanu
   aktywności — badge "nieaktywny" to informacja o stanie, nie kontrolka, i
   może zostać wygaszony razem z resztą wiersza. */
.ft-tpl-inactive .ft-row-actions { opacity: 1; }

/* ---- 29. Ustawienia + Członkowie (P6/T11, scalone w P7a/H3) ------------
   Cutover w miejscu (`/settings`) — makieta Ustawienia.dc.html (spec P6 §8).
   Do P7a/zadania 4 Członkowie żyli na własnej trasie (`/members`), BEZ H3;
   granica H3 (spec P7a §6) scala tę sekcję w kotwice Ustawień
   (#czlonkowie) — dawna trasa /members przekierowuje tu teraz przez
   LegacyRoutes.razor. Selektory .ft-members-*/.ft-member-*/.ft-role-pill-*
   niżej w tym bloku pochodzą z usuniętej ProjectMembers.razor bez zmian
   wizualnych — zmieniło się tylko GDZIE żyją (wewnątrz .ft-settings-section
   zamiast na osobnej stronie).

   Kotwice (.ft-settings-anchor) to zwykłe <a href="#..."> bez JS —
   scroll-margin-top na .ft-settings-section kompensuje ewentualny naklejony
   nagłówek powłoki, żeby cel kotwicy nie chował się pod nim. "Aktywny"
   stan pierwszej kotwicy (Ogólne) jest STATYCZNY, nie scrollspy: bez JS nie
   ma jak śledzić realną pozycję scrolla, a strona zawsze ładuje przewiniętą
   do góry (czyli faktycznie na sekcji Ogólne), więc statyczne oznaczenie nie
   kłamie. */
.ft-settings { margin-top: 4px; }
.ft-settings-anchors {
  display: flex; gap: 6px; margin-top: 16px; border-bottom: var(--ft-border); flex-wrap: wrap;
}
.ft-settings-anchor {
  font: var(--ft-type-small); color: var(--ft-text-3); padding: 8px 12px; text-decoration: none;
}
.ft-settings-anchor-active {
  font: 600 13px/18px var(--ft-font); color: var(--ft-accent-text);
  border-bottom: 2px solid var(--ft-accent); padding-bottom: 6px;
}
.ft-settings-section {
  margin-top: 20px; background: var(--ft-surface); border: var(--ft-border);
  border-radius: var(--ft-r-lg); overflow: hidden; scroll-margin-top: 76px;
}
.ft-settings-section-head {
  display: flex; align-items: center; gap: 10px; padding: 14px 24px; border-bottom: var(--ft-border);
}
.ft-settings-section-head h2 {
  font: var(--ft-type-h3); margin: 0; color: var(--ft-text-1);
}
.ft-settings-section-body { padding: 20px 24px; }
.ft-settings-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px;
}
/* `1fr` to w istocie `minmax(auto, 1fr)`, a `auto` w MINIMUM toru bierze
   `min-width: auto` dziecka, czyli jego rozmiar min-content. Tor potrafi więc
   urosnąć PONAD kontener, zamiast zmusić dziecko do skurczenia się — i wtedy
   rozpycha wszystkie pozostałe pola siatki, nie tylko to winne.
   Zmierzone w P9/T7 na oknie 390px: pole treści sekcji ma 308px, a
   `grid-template-columns` wyliczało 338,266px. Winowajcą jest bezklasowe
   opakowanie pola Slug w ProjectSettings.razor (~L180): jego min-content to
   338px, bo `.ft-settings-slugbox` jest mono + `white-space: nowrap`, a ołówek
   obok dokłada swoje. Własne `min-width: 0` slugboxa tego NIE ratuje — ono
   pozwala mu się kurczyć, gdy rodzic ma już szerokość, ale nie zmniejsza jego
   wkładu min-content do rodzica (nadpisuje minimum automatyczne, nie sufit).
   Skutek przed naprawą: pole „Nazwa projektu" kończyło się na 379 (30px za
   krawędzią treści), a ołówek Slug wystawał 5px poza kartę i był przez nią
   przycinany (`.ft-settings-section` ma `overflow: hidden`). Po naprawie tor
   ma 308px, a ołówek kończy się na 349, czyli dokładnie na krawędzi treści.
   `.ft-social-grid` dostaje to samo prewencyjnie — ma ten sam kształt
   (`1fr 1fr` → `1fr`) i to samo ryzyko: `.ft-social-meta` wewnątrz karty jest
   mono + `nowrap` (nazwa konta + „token do <data>"), więc min-content karty
   rośnie z długością nazwy konta. Łańcuch kurczenia jest już gotowy
   (karta → `.ft-social-body` z `min-width: 0` → `.ft-social-meta` z elipsą),
   brakowało tylko zdjęcia `min-width: auto` z samej karty. */
.ft-settings-grid > *,
.ft-social-grid > * { min-width: 0; }
.ft-settings-footer {
  display: flex; align-items: center; gap: 12px; padding: 14px 24px; border-top: var(--ft-border);
}
.ft-settings-saved {
  font: var(--ft-type-small); color: var(--ft-success-deep);
  display: inline-flex; align-items: center; gap: 5px;
}

/* Slug — makieta rysuje jako readonly, w kodzie pencil-to-edit (ChangeProjectSlug
   istnieje, rozszerzenie ponad makietę — spec P6 §8). */
/* `display: block`, NIE `flex` (P9/T7, zmierzone na zrzucie elementu przy
   390px). Elipsa renderuje się wyłącznie na treści INLINE bloku, a kontener
   flex kontekstu inline nie tworzy — `text-overflow: ellipsis` był tu martwy
   od początku. Repozytorium już to wie: komentarz przy `.ft-social-meta` mówi
   wprost, że „display:flex zabiłoby tam elipsę". Do P9 nie było tego widać,
   bo tor siatki był przewymiarowany (patrz `min-width: 0` wyżej) i tekst się
   mieścił; po zwężeniu toru do 308px slugbox dostaje 274px na 304px treści i
   URL urywał się w połowie słowa BEZ wielokropka („…/my-first-proje"), zamiast
   „…/my-first-pr…". `align-items`/`gap` odchodzą razem z flexem i nic nie
   tracą: slugbox nie ma dzieci elementowych, tylko węzeł tekstowy. */
.ft-settings-slugbox {
  display: block; border: var(--ft-border); border-radius: var(--ft-r-md);
  padding: 10px 13px; font: var(--ft-type-mono); font-size: 13px; color: var(--ft-text-2);
  background: var(--ft-surface-2); flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.ft-settings-slug-warning {
  font: var(--ft-type-micro); color: var(--ft-warning-deep); margin-top: 6px;
}

/* Połączenia social — karty 2-kol (spec P8: kropka brandu + stany zdrowia
   spójne z PlatformHealth, 7 dni/RequiresReauth). Dzieci tej siatki dostają
   `min-width: 0` razem z `.ft-settings-grid` wyżej — powód i pomiar tam. */
.ft-social-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.ft-social-card {
  border: var(--ft-border); border-radius: var(--ft-r-md); padding: 14px 16px;
  display: flex; align-items: center; gap: 12px;
}
.ft-social-card-warning { border: 1px solid var(--ft-warning-deep); background: var(--ft-warning-tint); }
.ft-social-dot { width: 10px; height: 10px; border-radius: var(--ft-r-full); flex-shrink: 0; }
.ft-social-body { flex: 1 1 auto; min-width: 0; }
/* Nazwa platformy + pigułka stanu w jednym wierszu. Odstęp MUSI dać kontener
   (gap), bo w Razor status stoi w gałęzi `@if` tuż za nazwą, a Razor obcina
   biały znak na granicy bloku — nazwa i status sklejały się w jedno słowo
   (wejścia P8 poz. 21). `flex-wrap`, żeby przy wąskiej karcie status zszedł
   pod nazwę zamiast rozpychać `.ft-social-body`. */
.ft-social-name {
  font: 600 14px/20px var(--ft-font);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.ft-social-status-ok {
  display: inline-flex; align-items: center; gap: 4px; font: var(--ft-type-micro); color: var(--ft-success-deep);
}
.ft-social-status-warn {
  display: inline-flex; align-items: center; gap: 4px; font: var(--ft-type-micro); color: var(--ft-warning-deep);
}
/* TRZECI stan, dołożony w P8: „Nie połączono". Ani sukces, ani ostrzeżenie —
   brak połączenia to punkt wyjścia każdego projektu, nie usterka, i karta
   też go tak traktuje (`IsWarningCard` = NeedsReauth || IsExpiringSoon, więc
   ramki ostrzeżenia nie dostaje). Do P8 markup wieszał tu klasę `-warn`
   i gasił ją inline'owym kolorem spoza słownika (mapa w tokens.css przy
   --ft-decor) — nadpisem, który wygrywał kaskadę i dopiero potem liczył się
   jako `unset`, czyli stan renderował się w kolorze rodzica (--ft-text-1),
   ani bursztynowo, ani szaro. */
.ft-social-status-idle {
  display: inline-flex; align-items: center; gap: 4px; font: var(--ft-type-micro); color: var(--ft-text-3);
}
.ft-social-meta {
  font: var(--ft-type-mono); font-size: 11px; color: var(--ft-text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ft-social-card-warning .ft-social-name,
.ft-social-card-warning .ft-social-meta { color: var(--ft-warning-deep); }
.ft-social-toggle { margin-top: 4px; }

@media (max-width: 1100px) {
  .ft-settings-grid, .ft-social-grid { grid-template-columns: 1fr; }
}

/* Strefa ryzyka — obrys danger na CAŁEJ sekcji, nie tylko nagłówku
   (mockup Ustawienia.dc.html L129: "border: 1px solid var(--ft-danger-deep)"
   na sekcji, nie na .ft-settings-section-head). */
.ft-settings-risk { border: 1px solid var(--ft-danger-deep); }
/* flex-wrap (P14 Z3, wejścia P10 poz. 123): blok tekstu ma inline
   min-width: 200px (ProjectSettings.razor), a przycisk MudBlazora
   min-width: 64px i nie zejdzie pod własną etykietę. Na 320 px wiersz daje
   238 px wewnątrz paddingu, a 200 + 14 + 64 = 278 wypychało przycisk 16 px
   za obrys sekcji, podczas gdy flex-shrink ściskał go poniżej etykiety
   (zmierzone na gałęzi archiwalnej, sonda v1: wiersz scrollWidth 302 > 286,
   „Archiwizuj” 54 px w polu 44 px — także na 360 px). Zawijanie zrzuca
   przycisk pod tekst z jego naturalną szerokością — ten sam kształt, który
   ma już .ft-profile-row (sekcja 26); od ok. 400 px wiersz zostaje
   jednoliniowy. Pomiar po zmianie: probes/overflow.mjs, 320 i 360 px. */
.ft-settings-risk-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px; padding: 14px 24px; border-bottom: var(--ft-border);
}
.ft-settings-risk-row:last-child { border-bottom: none; }
.ft-settings-risk-title { font: 600 14px/20px var(--ft-font); }
.ft-settings-risk-title-danger { color: var(--ft-danger-deep); }
.ft-settings-risk-desc { font: var(--ft-type-small); color: var(--ft-text-3); }

/* Karta-blokada nie-właściciela (makieta L142-148, decyzja D4: widoczna,
   zablokowana, NIE znika). h2 wewnątrz — jedyny <h1> strony zostaje w
   PageHeader (PageHeadingTests wymaga dokładnie jednego). */
.ft-settings-locked {
  margin-top: 20px; background: var(--ft-surface); border: var(--ft-border); border-radius: var(--ft-r-xl);
  padding: 64px 32px; display: flex; flex-direction: column; align-items: center; gap: 16px; text-align: center;
}
.ft-settings-locked-icon {
  display: grid; place-items: center; width: 52px; height: 52px; border-radius: 999px;
  background: var(--ft-warning-tint); color: var(--ft-warning-deep); flex-shrink: 0;
}
.ft-settings-locked-title { font: var(--ft-type-h2); margin: 0; }
.ft-settings-locked-body {
  font: var(--ft-type-body); color: var(--ft-text-3); margin: 8px auto 0; max-width: 48ch;
}
.ft-settings-locked-note { font: var(--ft-type-micro); color: var(--ft-text-3); }

/* Członkowie — sekcja Ustawień od P7a/H3 (dawniej własna trasa /members,
   patrz komentarz nagłówkowy tego bloku). Brak własnego margin-top: żyje
   teraz wewnątrz .ft-settings-section, którego 20px góra i tak ustawia
   rytm pionowy między sekcjami — .ft-members zostaje jako marker treści
   (m.in. dla testów), nie jako wrapper strony. */
.ft-members-count { font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-members-list {
  background: var(--ft-surface); border: var(--ft-border); border-radius: var(--ft-r-lg); overflow: hidden;
}
.ft-member-row { display: flex; align-items: center; gap: 14px; padding: 12px 24px; }
.ft-member-text { flex: 1 1 auto; min-width: 160px; }
/* Imię i nazwisko + znacznik „(Ty)" — ten sam powód co przy .ft-social-name:
   znacznik żyje w gałęzi `@if` tuż za nazwiskiem, więc odstęp musi dać gap,
   nie biały znak w markupie (wejścia P8 poz. 21). `baseline`, bo znacznik jest
   mniejszy od nazwiska i wyrównany do środka odstawał od linii pisma. */
.ft-member-name {
  font: 600 14px/20px var(--ft-font);
  display: flex; align-items: baseline; gap: 6px;
}
.ft-member-you { font: var(--ft-type-micro); color: var(--ft-text-3); }
.ft-member-email {
  font: var(--ft-type-mono); font-size: 11px; color: var(--ft-text-3);
}

/* Pigułka roli — statyczna (Właściciel, span), interaktywna (pozostali u
   właściciela-widza, MudSelect z klasą ft-role-pill na korzeniu — komponent
   wygrywa funkcjonalnie, klasa tylko przyciasnia go do kształtu pigułki), i
   statyczna-tylko-do-odczytu (pozostali u nie-właściciela-widza, span —
   P7a/H3, zadanie 4: kontrolki owner-only nie renderują się wcale dla
   nie-właściciela, więc ta trzecia odmiana nigdy nie jest MudSelect). */
.ft-role-pill {
  font: var(--ft-type-micro); border-radius: var(--ft-r-full); padding: 4px 12px; flex-shrink: 0;
}
.ft-role-pill-owner { background: var(--ft-accent-tint); color: var(--ft-accent-text); }
.ft-role-pill-static { background: var(--ft-surface-2); color: var(--ft-text-2); }
.ft-role-pill.mud-select { max-width: 140px; }
.ft-role-pill.mud-select .mud-input { border: var(--ft-border); border-radius: var(--ft-r-full); padding: 2px 10px; }
.ft-role-pill.mud-select .mud-input:before,
.ft-role-pill.mud-select .mud-input:after { display: none; }

.ft-member-delete {
  color: var(--ft-text-3); transition: background 120ms, color 120ms;
}
.ft-member-delete:hover { background: var(--ft-danger-tint); color: var(--ft-danger-deep); }

@media (max-width: 760px) {
  .ft-member-row { flex-wrap: wrap; }
}

/* Szkielet Ustawień (przegląd jakości T11, poprawka 1) — .ft-skel-card (104px,
   ogólny wariant §6a) byłby drastycznie za niski wobec realnej strony
   (~980px: Ogólne z 3 polami + workflow zatwierdzania, dwie karty social,
   strefa ryzyka) — skok layoutu przy ładowaniu (ui-ux.md "szkielet
   mierzony z realnej treści"). Pięć bloków, sekcja-po-sekcji (P7a/H3,
   zadanie 4: Członkowie dołożone jako piąty, między Ogólne a Połączenia —
   ten sam porządek co realne sekcje strony), ten sam wzorzec co
   .ft-skel-auditrow/.ft-skel-tplrow: wysokości wyliczone z realnych metryk
   tego pliku (nagłówek sekcji = 14px*2 padding + h3 24px linia = 52px;
   MudBlazor Outlined+Dense pole ~40px + wiersz helper/counter ~20px = ~60px
   na pole; wiersz Członka .ft-member-row = ten sam floor 57px co
   .ft-skel-row/ListRow) — bez żywego pomiaru DOM (brak dostępu do
   przeglądarki w tym środowisku), więc to najlepsze uzasadnione
   przybliżenie, nie odczyt piksel-idealny.
   Margin-top każdego bloku odtwarza margines realnej sekcji
   (.ft-settings-section: 20px; pasek kotwic: 16px), żeby stackowanie
   szkieletów oddawało realny rytm pionowy strony. */
.ft-skel-settings-anchors { height: 36px; margin-top: 16px; border-radius: var(--ft-r-md); }
.ft-skel-settings-general { height: 420px; margin-top: 20px; border-radius: var(--ft-r-lg); }
/* Nagłówek sekcji 52px + 3 wiersze Członka × 57px (floor .ft-skel-row) = 223px. */
.ft-skel-settings-members { height: 223px; margin-top: 20px; border-radius: var(--ft-r-lg); }
.ft-skel-settings-social { height: 250px; margin-top: 20px; border-radius: var(--ft-r-lg); }

/* ---- 30. Przełącznik widoku Lista/Kalendarz (Posty, P7a/H2, zadanie 8) -----
   Dwa segmenty <a href> (prawdziwe linki — nawigacja między dwiema trasami
   TEGO SAMEGO komponentu, ContentPosts.razor), wg makiety
   mockups/flowtent-design/project/Posty.dc.html (role="group" aria-label
   "Widok" L87/L265). role="group" grupuje je dla czytnika ekranu jako
   PARĘ przełączników, nie dwa niezależne linki nawigacyjne — aria-current
   "page" na aktywnym segmencie niesie stan tak samo jak MudNavLink robi to
   w ShellNav. */
.ft-view-switch {
  display: inline-flex;
  border: var(--ft-border);
  border-radius: var(--ft-r-md);
  overflow: hidden;
  font: var(--ft-type-micro);
}
.ft-view-switch-item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 13px;
  color: var(--ft-text-2);
  text-decoration: none;
  cursor: pointer;
}
.ft-view-switch-item + .ft-view-switch-item { border-left: var(--ft-border); }
.ft-view-switch-item.active { background: var(--ft-accent-tint); color: var(--ft-accent-text); }

/* ---- 31. Stepper miesiąca w wierszu presetu (Posty, P8 §6 poz. 22) --------
   Styl etykiety mieszkał do T12 w atrybucie style="" na <span>; przeniesiony
   tutaj, bo min-width potrzebuje drugiego wariantu, a inline'owego nie da się
   przebić regułą arkusza bez !important.
   Wariant -blank zdejmuje min-width, gdy MonthLabel nic nie zwraca — czyli
   ZAWSZE przed wyborem presetu „Miesiąc", więc przy każdym wejściu na listę.
   Zmierzone sondą w T12 na danych dev: wiersz presetu niesie osiem chipów,
   przełącznik widoku i stepper; puste 110 px etykiety przepychało go poza
   krawędź i stepper zawijał się do drugiej linii (wiersz 70 px zamiast 32 px)
   przy 1440 i 1280 px — mieścił się dopiero przy 1600. Klasa, nie :empty:
   Selectors 3 nie dopasowuje :empty do elementu z pustym węzłem tekstowym, a
   to, czy Blazor go wyemituje, nie jest kontraktem, na którym można oprzeć
   układ. Element zostaje w DOM także pusty — aria-live musi istnieć PRZED
   zmianą, żeby ją ogłosić. */
.ft-month-label {
  min-width: 110px;
  text-align: center;
  color: var(--ft-text-2);
}
.ft-month-label-blank { min-width: 0; }
.ft-skel-settings-risk { height: 220px; margin-top: 20px; border-radius: var(--ft-r-lg); }

/* ---- 32. Szkielet stron szczegółów (P13/T2, spec §4.4) --------------------
   Jeden region statusu (.ft-detail-skel) z blokami .ft-skel-block. Odstęp 16 px
   między nagłówkiem a treścią to margines .ft-pagehead. Siatka kolumn oddaje
   MudGrid strony szczegółów posta: poniżej md (960 px, próg MudBlazor) jedna
   kolumna, od md podział 9/3 z rynną 24 px (Spacing="3", padding pozycji 12 px
   z każdej strony); 20 px między blokami w kolumnie jak w makiecie
   Szczegoly Posta.dc.html (stan ładowania). */
.ft-detail-skel { display: flex; flex-direction: column; gap: 16px; }
.ft-detail-skel-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; }
.ft-detail-skel-col { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
@media (min-width: 960px) {
  .ft-detail-skel-grid { grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); gap: 24px; }
}

/* Prawa kolumna szczegółów posta (P13 Z14, spec §7.8). Do P13 styl inline
   `position: sticky; top: 16px` liczył odstęp od krawędzi viewportu, a pasek
   górny jest position: fixed o wysokości --mud-appbar-height (52 px,
   FlowtentTheme.LayoutProperties.AppbarHeight) — przyklejona kolumna chowała
   górę pod paskiem, zmierzone sondą DOM w P13/T1 (notatki koordynatora): przy
   1440 px po przewinięciu o 188 px górna krawędź kolumny stała na 16 px, dolna
   krawędź paska na 52 px, zakryte 36 px, a punkt przy górze kolumny trafiał
   w pasek. Wysokość z tej samej zmiennej i z tym samym zapasem 52px co
   .ft-shell-main i .ft-loadbar; tokenu --ft-topbar-h nie ma. */
.ft-detail-rail { position: sticky; top: calc(var(--mud-appbar-height, 52px) + 16px); }
