/* ==========================================================================
   profile.css — the profile hub (/u/<username>/)
   ==========================================================================

   Scope
     This page and its partials only, and it is the ONLY stylesheet they load:
     pages/profile.html pulls this instead of user.css, so the banner header,
     the tab rails, the avatar and banner pickers, the History lists and the
     settings forms all live here.

     Some rules are therefore twins of ones in user.css, which still serves
     /user/profile/, /user/settings/ and the public profile. Changing a shared
     component (the avatar picker, the pager) means changing it in both.

   Design tokens
     Color, shadow and border tokens are owned by theme_bridge.css. Do not
     declare a :root or [data-theme] block here, and do not hard-code a color —
     reference a semantic token instead. The exceptions are the two banner
     gradients, which are fixed artwork rather than theme surfaces.

   Contents
     1.  Page frame
     2.  Shared portal chrome            (fields, buttons, modal shell)
     3.  Pickers                         (avatar, banner, theme)
     4.  History lists and pager
     5.  Profile header                  (banner, avatar, identity, follow)
     6.  Tab rails and panes
     7.  Photo gallery
     8.  Settings cards and fields
     9.  Responsive — tablet and below   (≤ 768px)
     10. Responsive — narrow             (≤ 576px)
     11. Responsive — phone              (≤ 470px)
     12. Responsive — from tablet up     (≥ 768px)
     13. Responsive — desktop            (≥ 1024px)
     14. Reduced motion

     Sections 9-14 hold every media query in the file. The max-width ranges
     nest, so they run widest first: the narrower a range is, the later it has
     to sit in order to win.
   ========================================================================== */


/* ==========================================================================
   1. PAGE FRAME
   ==========================================================================
   The column. .profile-content is this page's .home-page-wrapper: it owns the
   1100px width and the 16px gutter, and .container around it stays full-bleed
   so it can go on painting the page surface. */

/* Nothing paints a surface on this page except the cards themselves, so the
   gaps between the banner, the tab rail and the panes show the page (or the
   wallpaper) straight through. Both containers are cleared: the outer one
   wraps the section, the inner one wraps the column. */
.container:has(#user-portal),
#user-portal .container {
    background: transparent !important;
}

/* ── Profile Content ──────────────────────────────────────────
   The page's content column. 1100px centred and nudged right by 4rem on
   desktop (see the media query further down) — the same geometry as
   .gallery-container, .projects-page and .footer-content, so the hub's edges
   line up with the footer and with every other page. */
.profile-content {
    position: relative;
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    padding: 2rem 16px 40px;
    box-sizing: border-box;
    min-height: 500px;
    background: transparent;
}


/* ==========================================================================
   2. SHARED PORTAL CHROME
   ==========================================================================
   Fields, buttons and the modal shell the pickers in section 3 sit in. */

/* Matches .home-page-wrapper so the portal column lines up with the home /
   gallery layout (same max-width, centring and side padding). */
/* Full-bleed, like .container on every other page: it paints the page
   surface, and .profile-content below is the column that carries the
   1100px width and the 16px gutter. Both used to pay that gutter, which
   left the content 32px in from each edge on a phone. */
#user-portal .container {
    width: 100%;
    margin: 0 auto;
    padding-left: 0;
    padding-right: 0;
    padding-top: 0 !important;
    box-sizing: border-box;
}


.form-control {
    background: var(--surface-inset) !important;
    border: 1px solid var(--border-strong) !important;
    color: var(--text-primary) !important;
    border-radius: 12px !important;
    padding: 12px 15px !important;
    transition: all 0.3s ease !important;
}


.form-control:focus {
    border-color: var(--border-focus) !important;
    box-shadow: 0 0 0 3px var(--accent-glow) !important;
    background: var(--surface-inset) !important;
}


.btn-portal-primary {
    background: var(--accent-primary) !important;
    color: var(--text-on-accent) !important;
    font-weight: var(--fw-bold) !important;
    border-radius: 12px !important;
    padding: 12px !important;
    border: none !important;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.3s ease !important;
}


.btn-portal-primary:hover {
    background: var(--accent-primary) !important;
    filter: brightness(1.1);
    transform: translateY(-2px);
    box-shadow: 0 10px 20px var(--accent-glow) !important;
}


/* Premium Public Profile styling */
/* On the hub these cards ARE the tab panes, so they fill the same column
   the header and tab bar do rather than sitting in a narrow reading
   column like they do on the public profile. */
.profile-hero-card {
    background: var(--surface-raised);
    border: 1px solid var(--border-default);
    border-radius: 12px;
    box-shadow: var(--shadow-md);
}


/* Privacy Lock Screen */
.privacy-lock-card {
    padding: 5rem 2rem;
    text-align: center;
    background: var(--surface-raised);
    border-radius: 24px;
    border: 1px solid rgba(255, 64, 64, 0.2);
    box-shadow: 0 8px 32px 0 rgba(255, 64, 64, 0.05);
    backdrop-filter: blur(12px);
}


.lock-icon-glow {
    font-size: 4.5rem;
    background: linear-gradient(to right, #ef4444, #f59e0b);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    filter: drop-shadow(0 0 15px rgba(239, 68, 68, 0.4));
    margin-bottom: 2rem;
    animation: pulseLock 2s infinite ease-in-out;
}


.modal {
    /* No z-index here: main.css lifts modals over the sidebar, and 1050
       put this one back underneath it. */
    display: none;
}


.modal-content {
    background: var(--surface-raised);
    border: 1px solid var(--border-default);
    color: var(--text-primary);
    border-radius: 12px;
    box-shadow: var(--shadow-lg);
    padding: 5px;
}


.modal-dialog {
    max-width: 650px;
    /* Matching the settings box width */
    margin: auto !important;
}


/* ==========================================================================
   3. PICKERS — AVATAR, BANNER, THEME
   ==========================================================================
   The avatar picker is a search field over a scrolling grid, filled a page at
   a time from the OpenavaBan API, with an upload alternative beside it. Banner
   presets are wide, so they list as rows rather than tiling like the round
   avatars do. The theme cards at the foot are the Appearance sub-tab. Their
   shell is the .modal rule in section 2. */
.banner-list {
    display: grid;
    gap: 12px;
    padding: 16px 5px;
}


.banner-card {
    display: block;
    width: 100%;
    padding: 0;
    border: 1px solid var(--border-default);
    border-radius: 12px;
    overflow: hidden;
    background: var(--surface-overlay);
    cursor: pointer;
    transition: border-color 0.18s ease, transform 0.18s ease;
}


.banner-card:hover,
.banner-card:focus-visible {
    border-color: var(--highlight);
    transform: translateY(-2px);
}


.banner-card img {
    display: block;
    width: 100%;
    height: 92px;
    object-fit: cover;
}


/* Wide like the header it becomes, not round like an avatar. */
.banner-upload-preview {
    margin: 0 auto 16px;
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid var(--border-default);
    background: var(--surface-overlay);
}


.banner-upload-preview img {
    display: block;
    width: 100%;
    height: 120px;
    object-fit: cover;
}


/* Choose / Upload, the two ways into the modal. Same pill treatment as
   the hub's own tab bar, at the size this narrower box can carry. */
.avatar-modes {
    display: flex;
    gap: 6px;
    margin-bottom: 16px;
    padding: 4px;
    border-radius: 12px;
    background: var(--surface-overlay);
}


.avatar-mode {
    flex: 1 1 0;
    padding: 8px 12px;
    border: none;
    border-radius: 9px;
    background: transparent;
    color: var(--text-muted);
    font-size: var(--fs-400);
    font-weight: var(--fw-semibold);
    cursor: pointer;
    transition: background 0.18s ease, color 0.18s ease;
}


.avatar-mode.active {
    background: var(--surface-raised);
    color: var(--text-primary);
    box-shadow: var(--shadow-sm);
}


.avatar-upload-field {
    margin-bottom: 14px;
}


.avatar-upload-label {
    display: block;
    margin-bottom: 6px;
    color: var(--text-primary);
    font-size: var(--fs-400);
    font-weight: var(--fw-semibold);
}


/* The API refuses an upload without these, so they are marked before the
   upload rather than explained after it. */
.avatar-required {
    color: var(--danger-500, #ef4444);
}


.avatar-upload-hint {
    margin: 6px 0 0;
    color: var(--text-muted);
    font-size: var(--fs-200);
}


.avatar-upload-preview {
    margin: 0 auto 16px;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    border: 1px solid var(--border-default);
    background: var(--surface-overlay);
}


.avatar-upload-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}


.avatar-upload-note {
    margin: 0 0 14px;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--surface-overlay);
    color: var(--text-muted);
    font-size: var(--fs-200);
    text-align: center;
}


.avatar-search-wrap {
    position: relative;
    margin-bottom: 5px;
}


.avatar-search-icon {
    position: absolute;
    top: 50%;
    left: 14px;
    transform: translateY(-50%);
    color: var(--text-muted);
    font-size: 0.85rem;
    pointer-events: none;
}


.avatar-search {
    width: 100%;
    padding: 10px 14px 10px 36px;
    border-radius: 30px;
    border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
    background: var(--input-bg, rgba(255, 255, 255, 0.05));
    color: var(--text-primary);
    font-size: var(--fs-400);
    outline: none;
}


.avatar-search:focus {
    border-color: var(--highlight);
}


.avatar-search::placeholder {
    color: var(--text-muted);
}


/* Caps the modal's height so paging in more avatars scrolls the grid
   instead of growing the dialog past the viewport. */
.avatar-results-scroll {
    max-height: 46vh;
    overflow-y: auto;
    margin: 0 -5px;
    padding: 0 5px;
}


.avatar-status {
    margin: 0;
    padding: 6px 0 12px;
    text-align: center;
    font-size: var(--fs-300);
    color: var(--text-muted);
}


.avatar-status:empty {
    display: none;
}


.avatar-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(85px, 1fr));
    gap: 20px;
    padding: 25px 5px;
    justify-content: center;
}


.item-avatar {
    position: relative;
    cursor: pointer;
}


.item-avatar:hover {
    transform: none !important;
}


/* The one marker for "this is the one you are wearing", shared by both grids.
   A ring rather than the badge that used to sit in an avatar's corner: it
   covered part of the very picture it was pointing at, and the banner grid
   never had one at all. .item-avatar img already carries a transparent 2px
   border, so nothing shifts when this lands. */
.item-avatar.active img,
.banner-card.active {
    border-color: var(--highlight);
    box-shadow: 0 0 0 2px var(--highlight);
}


.item-avatar img {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid transparent;
}


/* Appearance / Theme Selector */
.appearance-grid {
    display: grid;
    /* auto-fit, not a fixed 3: the list grows with each wallpaper, and a
       hardcoded column count strands the 4th card alone on its own row. */
    grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
    gap: 15px;
    margin-top: 10px;
}


.theme-card {
    cursor: pointer;
    border-radius: 12px;
    padding: 10px;
    background: var(--surface-overlay);
    border: 2px solid transparent;
    text-align: center;
    transition: all 0.2s ease;
}


.theme-card:hover {
    border-color: var(--border-default);
}


.theme-card.active {
    border-color: var(--highlight);
    background: var(--highlight-glow);
}


/* ==========================================================================
   4. ACCOUNT HISTORY AND PAGER
   ==========================================================================
   partials/history_list.html — the History tab's rail and its open pane.
   Views, likes, comments and account changes all print the same row: an
   icon, the thing's name, a sentence saying what happened, and how long ago.
   A row is a <details>, so the chevron turns and the rest drops out below it
   with no script. */

/* The History tab's own rail: Views, Likes, Comments, Account history. Not
   the .phub-subtab rail Settings uses - these are ordinary links that fetch
   the pane they name, so there is no sliding pill to keep in step with a
   pane the browser swapped.

   One set of links, two shapes: a side panel beside the list on a desktop and
   a row of tabs above it on a phone (the media query at the end of this
   block). The panel names each pane and says what it holds; the tab bar keeps
   the names and drops the descriptions, because a phone has no room for a
   sentence per tab. */
/* The card the History tab lives in carries no padding of its own (see
   pages/profile.html): the panel and its divider run from the top of the card
   to the bottom, and each column pads its own contents. */
/* The History card takes .phub-header's surface rather than the raised one
   every other card on the page uses: it is the header's counterpart at the
   bottom of the tab - the same width, the same rounded box - and the two
   reading as one material is what keeps the tab from looking like two
   unrelated panels. --surface-raised stays on the cards that sit inside
   this one. */
.hist-card {
    overflow: hidden;
    background-color: var(--surface-inset);
}


.hist-shell {
    display: flex;
    align-items: stretch;
    /* Tall enough that the divider has a card to run down when the record is
       short - an empty pane beside a full-height rule reads as a mistake. */
    min-height: 560px;
}


/* The heading and the links together are the side panel. They are separate
   elements because a phone turns the links into a row of tabs and stands the
   heading above them, which a heading inside the row could not do.

   Wide enough for the longest description to sit on two lines and no wider:
   past that the text stops short of the divider and the panel reads as a
   column of gaps. */
.hist-rail-wrap {
    flex: 0 0 252px;
    display: flex;
    flex-direction: column;
    border-right: 1px solid var(--border-default);
}


.hist-rail {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 12px 20px;
}


/* The heading and the sentence under it are the panel's own header, so the
   rule beneath them runs the whole width of the panel rather than stopping
   where the text does. */
.hist-rail-head {
    padding: 20px 20px 16px;
    border-bottom: 1px solid var(--border-default);
    text-align: center;
}


.hist-rail-title {
    margin: 0 0 6px;
    font-size: var(--fs-700);
    font-weight: var(--fw-bold);
    line-height: var(--lh-snug);
    color: var(--text-primary);
}



.hist-rail-tab {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 10px;
    border-radius: 10px;
    color: var(--text-primary);
    text-decoration: none;
    transition: background-color 0.2s ease;
}


.hist-rail-tab:hover {
    background-color: var(--surface-overlay);
    color: var(--text-primary);
}


/* The open pane keeps the hover wash, rather than wearing a border as well:
   a boxed row in a rail of four reads as a button somebody left pressed. */
.hist-rail-tab.active {
    background-color: var(--surface-overlay);
}


/* The glyph sits bare beside the name, at the size the name is set in, and
   lines up with the first line of it rather than with the block. It takes
   the row's own text colour and keeps it on the open row too - the wash is
   what says which pane is open. */
.hist-rail-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    padding-top: 1px;
    color: inherit;
    font-size: 1.05rem;
}


.hist-rail-body {
    min-width: 0;
}


.hist-rail-name {
    display: block;
    font-size: var(--fs-500);
    font-weight: var(--fw-bold);
    line-height: var(--lh-snug);
}


/* Two even lines rather than a long one and an orphan: the panel is as wide
   as the sentence needs and the sentence is balanced across it. */
.hist-rail-desc {
    display: block;
    margin-top: 2px;
    font-size: var(--fs-300);
    line-height: var(--lh-cozy);
    color: var(--text-muted);
    text-wrap: pretty;
}


.hist-pane {
    flex: 1;
    min-width: 0;
    padding: 20px 24px 24px;
    transition: opacity 0.15s ease;
}


/* While the next pane is on its way. A short fade rather than a spinner: the
   fetch is one query and usually lands before this is even noticed, and a
   pane that empties itself first would flash. */
.hist-pane.is-loading {
    opacity: 0.5;
    pointer-events: none;
}


/* Gallery / Blog / Project: the same inset pill rail as the tabs above the
   page and the Settings sub-tabs inside it (.phub-tabs, .phub-subtabs), at
   this rail's size. Three ways of reading one list is the same kind of
   control as four ways of reading one profile, and it should not arrive as a
   third shape - a row of loose pills read as chips to add up rather than as
   one choice of three.

   The one thing it does not take is the sliding indicator, which needs a
   script to follow the tab it is under. Here the open filter wears the pill
   itself - the same --surface-overlay and hairline ring the indicator is
   drawn with - so a rail rendered by the server alone still looks the
   part.

   It sits above the scroller rather than inside it, so it keeps its place
   while the record moves and nothing passes behind it - which is also why it
   can wear the same --surface-inset as the rails it matches. Stuck to the
   top of the scroller instead, tiles showed through the square corners its
   rounded ones leave behind, and hiding them would have meant an opaque
   surface this rail has no other reason to wear. */
.hist-filters {
    display: flex;
    gap: 4px;
    width: 100%;
    margin-bottom: 16px;
    padding: 4px;
    border-radius: 12px;
    background: var(--surface-inset);
    border: 1px solid var(--border-default);
    box-sizing: border-box;
    overflow-x: auto;
    scrollbar-width: none;
}


.hist-filters::-webkit-scrollbar {
    display: none;
}


.hist-filter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex: 1 1 0;
    min-width: 0;
    padding: 8px 14px;
    border: 0;
    border-radius: 9px;
    background-color: transparent;
    color: var(--text-muted);
    font-size: var(--fs-400);
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    text-decoration: none;
    transition: color 0.18s ease, background-color 0.18s ease;
}


.hist-filter:hover {
    color: var(--text-primary);
}


.hist-filter.active {
    background-color: var(--surface-overlay);
    box-shadow: 0 0 0 1px var(--border-default);
    color: var(--text-primary);
}


/* The record scrolls inside the card, not with the page: the rail, the intro
   and the toolbar hold still while the list moves under them, which is also
   what lets the loader watch a container rather than the window.

   The height is what is left of the screen once the topbar, the profile
   header and this pane's own chrome have taken theirs - a floor keeps it
   usable on a short window, and the cap stops a 4K screen from printing a
   hundred tiles before the loader ever fires.

   No overscroll-behavior here on purpose. The default, `auto`, is what lets
   the scroll carry on into the page once the list has run out at either end.
   This used to say `contain`, which stops that: reaching the top or the
   bottom of the record killed the gesture instead of passing it on, so the
   wheel appeared to jam and the page had to be grabbed somewhere else to
   move at all. Chaining is what a reader expects from a panel inside a page,
   and it costs nothing here - there is no pull-to-refresh to protect, and
   the infinite-scroll observer watches the container regardless.

   Desktop only. On a phone or tablet the box would be a scroller inside a
   scrolling page, with little more than the list's own height to scroll in,
   so there the record simply runs on with the page and the loader watches
   the window instead (see watchForMore in partials/history_list.html). */
@media (min-width: 1024px) {
    .hist-scroll {
        max-height: clamp(420px, calc(100vh - 260px), 820px);
        padding-right: 8px;
        margin-right: -8px;
        overflow-y: auto;
        scrollbar-width: thin;
        scrollbar-color: var(--border-strong) transparent;
    }
}


.hist-scroll::-webkit-scrollbar {
    width: 8px;
}


.hist-scroll::-webkit-scrollbar-track {
    background: transparent;
}


.hist-scroll::-webkit-scrollbar-thumb {
    background-color: var(--border-strong);
    border-radius: 4px;
}


.hist-scroll::-webkit-scrollbar-thumb:hover {
    background-color: var(--text-muted);
}


/* The record starts clear of whatever sits above it - the toolbar on its own,
   or the type buttons when the pane has them. */
.hist-body {
    margin-top: 18px;
}


/* Views and Likes are lists of pictures, so they print as pictures. Blog
   posts run one to a row: a cover image is a landscape, and three of those
   across a column are unreadable. */
.hist-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}


.hist-grid.hist-grid-wide {
    grid-template-columns: minmax(0, 1fr);
}


.hist-tile {
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
    border: 1px solid var(--border-default);
    border-radius: 10px;
    background-color: var(--surface-inset);
    overflow: hidden;
}


.hist-grid-wide .hist-tile {
    aspect-ratio: 16 / 9;
}


.hist-tile-gone {
    cursor: default;
}


.hist-tile-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}


.hist-tile-glyph {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: 1.6rem;
    opacity: 0.4;
}


/* The count sits on the picture, so it answers to the picture rather than to
   the theme: white over a scrim, in every mode. */
.hist-tile-count {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 24px 12px 10px;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0));
    color: #ffffff;
    font-size: var(--fs-300);
    font-weight: var(--fw-bold);
    line-height: var(--lh-none);
}


/* A comment prints under the post it was left on and a reply under the
   comment it answers, with the member's own words below either one - the way
   the comment section itself reads. */
.hist-thread {
    padding: 16px 0;
    border-bottom: 1px solid var(--border-default);
}


.hist-thread-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
}


.hist-thread-reply {
    margin: 12px 0 0 52px;
}


.hist-ava {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    background-color: var(--surface-overlay-strong);
    color: var(--text-primary);
    font-size: var(--fs-300);
    font-weight: var(--fw-bold);
}


.hist-ava-sm {
    width: 32px;
    height: 32px;
    font-size: var(--fs-100);
}


.hist-thread-body {
    flex: 1;
    min-width: 0;
}


/* The byline, in the two rows the comment section sets it in: the name on
   its own, then the handle and the age under it. Together they read as one
   block, which is why the second row sits tight under the first. */
.hist-thread-line {
    margin: 0;
    font-size: var(--fs-400);
    line-height: var(--lh-snug);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}


.hist-thread-name {
    color: var(--text-primary);
    font-weight: var(--fw-bold);
}


/* The badge after the name, and the dot that introduces it. Sized and
   coloured to match the discussion's own: [Admin] in the accent, [edited]
   receding into the muted ink. */
.hist-thread-dot {
    opacity: 0.4;
    font-size: var(--fs-300);
}


.hist-thread-badge {
    font-size: var(--fs-300);
    font-weight: var(--fw-semibold);
    color: var(--highlight);
}


.hist-thread-badge-quiet {
    font-weight: var(--fw-regular);
    color: var(--text-muted);
}


.hist-thread-meta {
    margin: 1px 0 0;
    font-size: var(--fs-200);
    line-height: var(--lh-snug);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}


.hist-thread-handle,
.hist-thread-when {
    color: var(--text-muted);
}


/* The words themselves, under the byline rather than run on from it: a
   comment is a paragraph, and reading it as the tail of a name works only
   while it is short. */
.hist-thread-text {
    margin: 5px 0 0;
    font-size: var(--fs-400);
    line-height: var(--lh-normal);
    color: var(--text-primary);
    overflow-wrap: anywhere;
    white-space: pre-line;
}


/* A picked GIF, at the size the comment section gives it. Capped in height as
   well as width: a tall one would otherwise push the rest of the record out
   of the pane on its own. */
.hist-thread-gif {
    display: block;
    max-width: 260px;
    max-height: 260px;
    width: auto;
    margin-top: 6px;
    border-radius: 10px;
}


.hist-thread-thumb {
    flex-shrink: 0;
    display: block;
    width: 46px;
    height: 46px;
    border: 1px solid var(--border-default);
    border-radius: 8px;
    overflow: hidden;
}


.hist-thread-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}


/* What the scroll loader watches for. It carries the spinner so the wait is
   visible, and the script takes it out when the list ends. */
.hist-sentinel {
    display: flex;
    justify-content: center;
    padding: 24px 0 8px;
}


.hist-spinner {
    width: 22px;
    height: 22px;
    border: 2px solid var(--border-strong);
    border-top-color: var(--highlight);
    border-radius: 50%;
    animation: hist-spin 0.7s linear infinite;
}


@keyframes hist-spin {
    to {
        transform: rotate(360deg);
    }
}


@media (prefers-reduced-motion: reduce) {
    .hist-spinner {
        animation-duration: 3s;
    }
}


/* Phones and tablets: the panel becomes a row of tabs above the list, the
   open one underlined rather than filled. The bar runs to both edges of the
   card so a tab half off the screen says it scrolls. */
@media (max-width: 1023px) {
    .hist-shell {
        display: block;
        min-height: 0;
    }

    .hist-rail-wrap {
        border-right: 0;
    }

    /* Four tabs, four equal shares of the width: every tab takes the same
       slice, so the gaps between them are identical without a gap property
       at all, and the bar runs edge to edge with nothing to scroll. */
    .hist-rail {
        flex-direction: row;
        gap: 0;
        padding: 0 12px;
        border-bottom: 1px solid var(--border-default);
    }

    /* No heading at this width. The panel it named is a bar of four tabs
       here, which says what the section is on its own, and a title above it
       spends a line of a small screen on saying it twice. The nav keeps its
       aria-label, so the section is still named for a screen reader. */
    .hist-rail-head {
        display: none;
    }

    .hist-rail-tab {
        flex: 1 1 0;
        min-width: 0;
        align-items: center;
        justify-content: center;
        gap: 8px;
        padding: 14px 0 12px;
        border: 0;
        border-bottom: 2px solid transparent;
        border-radius: 0;
        color: var(--text-muted);
        font-size: var(--fs-300);
        font-weight: var(--fw-bold);
        letter-spacing: var(--ls-wide);
        text-transform: uppercase;
    }

    .hist-rail-tab:hover,
    .hist-rail-tab.active {
        background-color: transparent;
        color: var(--text-primary);
    }

    .hist-rail-tab.active {
        border-bottom-color: var(--highlight);
    }

    .hist-rail-icon {
        width: auto;
        padding-top: 0;
        font-size: 0.95em;
    }

    /* A tablet has room for the name beside the glyph; the narrowest one
       ends it in an ellipsis rather than pushing the bar out of shape. */
    .hist-rail-name {
        font-size: inherit;
        font-weight: inherit;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* A sentence per tab has nowhere to go on a phone. */
    .hist-rail-desc {
        display: none;
    }

    /* The gap closes up at this width, so a narrow screen spends what it has
       on the pictures rather than on the space between them. */
    .hist-grid {
        gap: 6px;
    }

    /* Two across for the post grids - a gallery or project tile carries a
       title and a count, and a third of a narrow screen is too little to
       read them in. The member's own photos stay three across, the way a
       phone photo app prints them: those tiles are the picture alone.
       Blog is excluded by name rather than left to .hist-grid-wide: that
       rule and a bare :not() carry the same weight, and this one comes
       later, so it would have taken the blog's single column away. */
    .hist-grid:not(.hist-photo-grid):not(.hist-grid-wide) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .hist-pane {
        padding: 16px;
    }
}


/* Phones: the glyph carries the tab on its own. Four names at this width
   would each be an ellipsis, which names nothing. */
@media (max-width: 767px) {
    .hist-rail-tab {
        gap: 0;
    }

    .hist-rail-icon {
        font-size: 1.15rem;
    }

    /* A link with no text is a link with no name, so the name goes off the
       screen rather than out of the page. */
    .hist-rail-name {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
    }
}


.acct-history {
    margin-top: 4px;
}








/* The date heading a run of rows sits under ("This year", "2024"). */
.acct-history-heading {
    font-size: var(--fs-500);
    font-weight: var(--fw-bold);
    color: var(--text-primary);
    margin: 22px 0 10px;
}


.acct-history-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    border-radius: 12px;
    background-color: var(--surface-inset);
    border: 1px solid var(--border-default);
    margin-bottom: 8px;
    overflow: hidden;
}


.acct-history-row+.acct-history-row {
    margin-top: 0;
}












.acct-history-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background-color: var(--surface-overlay-strong);
    color: var(--text-primary);
    font-size: 0.95rem;
}




.acct-history-body {
    flex: 1;
    min-width: 0;
}


.acct-history-label {
    display: block;
    font-size: var(--fs-500);
    font-weight: var(--fw-bold);
    color: var(--text-primary);
    line-height: var(--lh-snug);
}


.acct-history-line {
    display: block;
    font-size: var(--fs-400);
    line-height: var(--lh-normal);
    color: var(--text-muted);
    /* A long bio is quoted in full in the row's detail; the summary keeps to
       three lines so one edit cannot push the rest of the page away. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}


/* The new value, which is the point of the row. */
.acct-history-value {
    color: var(--text-primary);
    font-weight: var(--fw-semibold);
    overflow-wrap: anywhere;
}


.acct-history-when {
    flex-shrink: 0;
    font-size: var(--fs-200);
    color: var(--text-muted);
    white-space: nowrap;
}














.acct-history-empty {
    text-align: center;
    padding: 48px 12px;
    color: var(--text-muted);
}


.acct-history-empty i {
    font-size: 2.4rem;
    opacity: 0.25;
    margin-bottom: 14px;
}


.acct-history-empty h6 {
    color: var(--text-primary);
    font-weight: var(--fw-semibold);
    margin-bottom: 6px;
}


.acct-history-empty p {
    font-size: var(--fs-400);
    margin-bottom: 0;
}


/* Premium Pagination Styling matching the Mockup */
.pagination-wrapper {
    display: flex;
    justify-content: center;
    align-items: center;
    /* The pager prints a window of pages rather than three, so a narrow card
       wraps them instead of overflowing. */
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 40px;
    margin-bottom: 10px;
}


/* Where the pager skipped a stretch of pages. Not a button - there is
   nothing to click - so it only has to line up with them. */
.pagination-gap {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    color: var(--text-muted);
    user-select: none;
}


.pagination-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 8px;
    background-color: var(--surface-inset);
    color: var(--text-primary);
    text-decoration: none;
    font-weight: var(--fw-semibold);
    font-size: var(--fs-500);
    border: 1px solid var(--border-default);
    transition: all 0.2s ease;
}


.pagination-btn:hover {
    background-color: var(--surface-overlay-strong);
    color: var(--text-primary);
    border-color: var(--border-strong);
}


.pagination-btn.active {
    background-color: var(--accent-primary) !important;
    color: var(--text-on-accent) !important;
    border-color: var(--accent-primary) !important;
}


.pagination-btn.disabled {
    opacity: 0.25;
    pointer-events: none;
    background-color: var(--surface-overlay);
}


/* ==========================================================================
   5. PROFILE HEADER
   ==========================================================================
   Banner, the avatar straddling its lower edge, the name / handle / bio
   block, and the Follow action pinned opposite them. */
/* The tab rail's own surface and edge - .phub-tabs is --surface-inset inside a
   --border-default border - so the card above the tabs and the rail below it
   are one material. Not the rail's sliding pill, which is --surface-overlay:
   that marks which tab is open and is a step lighter on purpose. */
.phub-header {
    border-radius: 18px;
    overflow: hidden;
    background: var(--surface-inset);
    border: 1px solid var(--border-default);
}


.phub-banner {
    position: relative;
    /* One height everywhere. The phone used to cut this to 160px, back when the
       header was an inset card; edge to edge, that read as a strip rather than
       a picture, and 200px is roughly 2:1 at a typical phone width. */
    height: 200px;
    /* Gradient underneath the banner picture, which the template always sets
       inline - the stock one when the member has not uploaded their own. */
    background:
    radial-gradient(120% 140% at 15% 0%, var(--highlight-glow), transparent 60%),
    linear-gradient(120deg, rgba(13, 148, 136, 0.35), rgba(30, 41, 59, 0.55));
    background-size: cover;
    background-position: center;
}


.phub-banner-actions {
    position: absolute;
    top: 12px;
    right: 12px;
    display: flex;
    align-items: center;
    gap: 8px;
}


/* The box only. Fill, edge and text colour come from the shared photo-overlay
   glass rule further down - these sit on the banner picture. */
.phub-icon-btn {
    width: 34px;
    height: 34px;
    border-radius: 10px;
    font-size: 0.85rem;
}


/* The avatar overlaps the banner, so the row is pulled up by half its height. */
/* Stacked, not side by side: the avatar sits on its own line straddling the
   banner's lower edge, with the name, handle and bio below it and the actions
   pinned to the right of that text. */
.phub-identity {
    position: relative;
    display: block;
    padding: 0 24px 22px;
}


.phub-avatar {
    position: relative;
    /* inline-flex keeps the click target the size of the picture rather than
       the width of the line it now owns. */
    display: inline-flex;
    flex-shrink: 0;
    /* Half the avatar's height, so it straddles the banner's lower edge. */
    margin-top: -90px;
}


/* 180px square, matching .circle-avatar-wrapper in home.css so a member's
   picture is the same size here as it is in My Circle. */
.phub-avatar img {
    width: 180px;
    height: 180px;
    border-radius: 50%;
    object-fit: cover;
    /* The ring is the header card it sits on, so the circle reads as cut out
       of the card rather than outlined in a colour of its own. --surface-inset
       is translucent on the dark themes, so the page has to sit under it for
       the two to composite into the shade the card actually renders - a border
       paints over the element's own background by default. */
    border: 4px solid var(--surface-inset);
    background: var(--surface-page);
}


/* Owner's avatar doubles as the picker trigger. */
.phub-avatar-editable {
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    border-radius: 50%;
}


.phub-avatar-editable:focus-visible {
    outline: 2px solid var(--highlight);
    outline-offset: 3px;
}


/* Hover swaps the uploaded photo for the picked avatar. The avatar sits on top
   and fades in; when it is the only picture it stays put and nothing moves. */
.phub-avatar-photo + .phub-avatar-alt {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 0.25s ease;
}


.phub-avatar:hover .phub-avatar-photo + .phub-avatar-alt,
.phub-avatar-editable:focus-visible .phub-avatar-photo + .phub-avatar-alt {
    opacity: 1;
}

/* Click-toggle: when .show-avatar is on the container, force avatar
   visible and hide the profile photo regardless of hover state. */
.phub-avatar-editable.show-avatar .phub-avatar-photo {
    opacity: 0 !important;
}

.phub-avatar-editable.show-avatar .phub-avatar-alt {
    opacity: 1 !important;
}


.phub-names {
    /* Leaves room for the actions parked on the right of this block. */
    padding: 14px 160px 0 0;
    min-width: 0;
}


.phub-name {
    margin: 0;
    font-size: var(--fs-1100);
    font-weight: var(--fw-bold);
    letter-spacing: -0.5px;
    word-break: break-word;
}


.phub-handle {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-500);
}


/* ==========================================================================
   6. TAB RAILS AND PANES
   ==========================================================================
   Panes are toggled by an .active class from the page's own JS rather than by
   Bootstrap, so a tab can also be opened from a header button or straight from
   the URL. Settings carries a second rail of the same design one size down.

   Same tab bar as the home page's Tech Stack and Achievements sections: an
   inset pill rail with a single indicator that slides behind the active tab
   rather than each tab painting its own background.

   Unlike home's, this rail spans the full column so its edges line up with the
   header card above it and the panes below - the tabs share the width out
   between them rather than the rail shrinking to fit the labels. */
.phub-tabs {
    position: relative;
    display: flex;
    gap: 4px;
    width: 100%;
    margin-bottom: 18px;
    padding: 5px;
    border-radius: 14px;
    background: var(--surface-inset);
    border: 1px solid var(--border-default);
    box-sizing: border-box;
    overflow-x: auto;
    scrollbar-width: none;
}

.phub-tabs::-webkit-scrollbar {
    display: none;
}

.phub-tab-indicator {
    position: absolute;
    top: 5px;
    bottom: 5px;
    left: 0;
    width: 0;
    border-radius: 10px;
    background: var(--surface-overlay);
    box-shadow: 0 0 0 1px var(--border-default);
    transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1),
    width 0.3s cubic-bezier(0.2, 0, 0, 1);
    pointer-events: none;
    z-index: 1;
}

.phub-tab {
    position: relative;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex: 1 1 0;
    min-width: 0;
    padding: 9px 18px;
    border: none;
    border-radius: 10px;
    background: transparent;
    color: var(--text-muted);
    font-size: var(--fs-500);
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    cursor: pointer;
    /* Anchors now, so that they carry a real address - the underline and the
       visited colour that come with one are not wanted here. */
    text-decoration: none;
    transition: color 0.18s ease;
}

.phub-tab i {
    font-size: 0.85rem;
}

.phub-tab:hover {
    color: var(--text-primary);
}

.phub-tab.active {
    color: var(--text-primary);
}


.phub-pane {
    display: none;
}


.phub-pane.active {
    display: block;
}


.phub-section-title {
    font-size: var(--fs-800);
    font-weight: var(--fw-bold);
    margin-bottom: 1rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
}


.phub-section-title i {
    color: var(--highlight);
    margin-right: 8px;
}


/* Activity totals, as the stat tiles of a dashboard rather than four
   coloured blocks. Bootstrap's grid does the four-across layout.

   Each tile is poured in its own colour, held in --phub-stat and set inline
   to the colour that type of event wears on the timeline, so a tile and its
   nodes there read as the same thing. Everything on the tile is then white:
   the fill is the identity, and a second hue on top of it would only fight
   the number.

   No card wraps the row - the tiles are the panels and sit straight on the
   page. No border either: at this saturation the edge of the tile is the
   colour ending, and a rule around it reads as a seam. */
.phub-stat {
    display: grid;
    grid-template-columns: 1fr auto;
    align-content: center;
    gap: 6px 12px;
    padding: 16px 18px;
    border-radius: 14px;
    background: var(--phub-stat);
    box-shadow: var(--shadow-md);
}


/* The label leads and the count answers it, which is the order they are read
   in - and it keeps the numbers on one baseline across the row however long
   the labels are. */
.phub-stat-label {
    grid-column: 1;
    margin: 0;
    /* White at a slight remove, so the label recedes behind the count
       without picking up a grey that would fight the tile's colour. */
    color: rgba(255, 255, 255, 0.9);
    font-size: var(--fs-300);
    font-weight: var(--fw-medium);
    line-height: var(--lh-snug);
}


.phub-stat-value {
    grid-column: 1;
    margin: 0;
    /* No tabular-nums: equal-width digits are for columns of figures that
       have to line up, and they leave a number this size looking gappy. */
    font-size: var(--fs-1100);
    font-weight: var(--fw-bold);
    line-height: 1.1;
    letter-spacing: -0.01em;
    color: var(--n-0);
}


/* The chip spans both rows and centres against them, so it reads as the
   tile's mark rather than as something sitting beside the label. */
.phub-stat-mark {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 11px;
    /* The chip is the tile's own colour lightened by the white over it, not
       a colour of its own: the fill under it is already the identity, and
       one hue per tile is what keeps four of them side by side readable. */
    background: rgba(255, 255, 255, 0.22);
    color: var(--n-0);
    font-size: 1rem;
}


/* Two across on a phone, so the label, the count and the chip have to fit a
   half-width tile: the count comes down a step and the chip with it. */
@media (max-width: 575px) {
    .phub-stat {
        padding: 14px;
        gap: 4px 10px;
    }

    .phub-stat-value {
        font-size: var(--fs-1000);
    }

    .phub-stat-mark {
        width: 32px;
        height: 32px;
        border-radius: 9px;
        font-size: 0.875rem;
    }
}


/* The Settings sub-tabs are the same inset pill rail as the tabs above them,
   one notch smaller - same sliding indicator, same behaviour, less weight, so
   the two rails read as a hierarchy rather than two different controls. */
.phub-subtabs {
    position: relative;
    display: flex;
    gap: 4px;
    width: 100%;
    margin-bottom: 24px;
    padding: 4px;
    border-radius: 12px;
    background: var(--surface-inset);
    border: 1px solid var(--border-default);
    box-sizing: border-box;
    overflow-x: auto;
    scrollbar-width: none;
}


.phub-subtabs::-webkit-scrollbar {
    display: none;
}


/* Shares .phub-tab-indicator with the main rail, so only the inset needs
   trimming to this rail's smaller padding. */
.phub-subtabs .phub-tab-indicator {
    top: 4px;
    bottom: 4px;
    border-radius: 9px;
}


.phub-subtab {
    position: relative;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex: 1 1 0;
    min-width: 0;
    padding: 8px 14px;
    border: none;
    border-radius: 9px;
    background: transparent;
    color: var(--text-muted);
    font-size: var(--fs-400);
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    cursor: pointer;
    text-decoration: none;
    transition: color 0.18s ease;
}


.phub-subtab:hover {
    color: var(--text-primary);
}


.phub-subtab.active {
    color: var(--text-primary);
}


.phub-subpane {
    display: none;
}


.phub-subpane.active {
    display: block;
}

/* The rails keep their labels on a phone. Same treatment as the home page's
   Tech Stack and Achievements tabs: the type and the padding come down, the
   items size to their own labels, and the rail itself scrolls if the four of
   them still will not fit. Hiding the labels and leaving four bare glyphs was
   the previous answer, and an icon alone does not say "Preference". */


/* Confirmation flash after the share button copies the profile link. */
.phub-icon-btn.copied {
    color: var(--highlight);
    border-color: var(--highlight);
}


/* ==========================================================================
   7. PHOTO GALLERY
   ==========================================================================
   ──────────────────────────────────────────────────────────────────
   Twin of the grid in user.css that /user/profile/ uses; see the note at
   the top of this file about the duplication.
   ----------------------------------------------------------------- */


/* ── PROFILE PHOTO GALLERY ───────────────────────────────────────────
   The editor grid on /user/profile/ and the read-only strip on a public
   profile. Both use the shared surface/border/accent tokens so they follow
   whatever theme (navy, light, wallpaper) is active.
   ------------------------------------------------------------------- */


.profile-gallery-item,
.profile-gallery-add {
    position: relative;
    /* Fills the Bootstrap column it sits in - a button would otherwise
       shrink-wrap its label instead of squaring off with the photos. */
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: 14px;
    overflow: hidden;
    margin: 0;
}


.profile-gallery-item {
    background: var(--surface-overlay);
    border: 1px solid var(--border-default);
}


.profile-gallery-item.is-primary {
    border-color: var(--highlight);
    box-shadow: 0 0 0 1px var(--highlight);
}


.profile-gallery-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}


/* The close control. Always visible on touch, where there is no hover to
   reveal it. */
/* Drag handle. touch-action: none only here, so a finger can still scroll the
   page from anywhere else on the tile. */
.profile-gallery-grip {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: var(--fs-200);
    cursor: grab;
    touch-action: none;
    opacity: 0;
    transition: opacity 0.18s ease, background 0.18s ease;
}


.profile-gallery-item:hover .profile-gallery-grip,
.profile-gallery-grip:focus-visible {
    opacity: 1;
}


.profile-gallery-grip:active {
    cursor: grabbing;
}


/* The cell being carried: lifted, faded, and out of the way of the pointer so
   elementFromPoint keeps reporting the tile underneath. */
.profile-gallery-cell.is-dragging .profile-gallery-item {
    opacity: 0.55;
    transform: scale(0.97);
    pointer-events: none;
}


.profile-gallery-cell.is-dragging {
    cursor: grabbing;
}


.profile-gallery-remove {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: var(--fs-400);
    line-height: var(--lh-none);
    cursor: pointer;
    transition: background 0.18s ease, transform 0.18s ease;
}


.profile-gallery-remove:hover,
.profile-gallery-remove:focus-visible {
    background: var(--danger-500, #ef4444);
    transform: scale(1.08);
}


.profile-gallery-tag,
.profile-gallery-promote {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 6px 8px;
    font-size: var(--fs-100);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--ls-wide);
    text-align: center;
    border: none;
    margin: 0;
}


.profile-gallery-tag {
    background: var(--highlight);
    color: var(--text-on-accent, #fff);
}


.profile-gallery-promote {
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    cursor: pointer;
    opacity: 0;
    transform: translateY(100%);
    transition: opacity 0.18s ease, transform 0.18s ease;
}


.profile-gallery-item:hover .profile-gallery-promote,
.profile-gallery-promote:focus-visible {
    opacity: 1;
    transform: translateY(0);
}


.profile-gallery-add {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: var(--surface-overlay);
    border: 1px dashed var(--border-strong);
    color: var(--text-muted);
    font-size: var(--fs-300);
    cursor: pointer;
    transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
}


.profile-gallery-add i {
    font-size: 1.4rem;
}


.profile-gallery-add:hover:not(:disabled) {
    border-color: var(--highlight);
    color: var(--highlight);
    background: var(--surface-overlay-strong);
}


.profile-gallery-add:disabled,
.profile-gallery-add.is-busy {
    cursor: wait;
    opacity: 0.6;
}

/* ==========================================================================
   8. SETTINGS CARDS AND FIELDS
   ==========================================================================
   ──────────────────────────────────────────────────────────────────
   Each settings section is its own titled card, so a long tab reads as a
   few labelled groups rather than one continuous form.
   ----------------------------------------------------------------- */
/* Top level in the Settings pane now, not nested inside a panel of their own,
   so they take the header card's surface - the same --card-bg, flat, no lift -
   and the pane's full width, matching the rail above. */
.phub-card {
    display: block;
    margin-bottom: 18px;
    padding: 22px;
    border: 1px solid var(--border-default);
    border-radius: 14px;
    background: var(--card-bg);
}


.phub-card:last-child {
    margin-bottom: 0;
}

.phub-card-head {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin-bottom: 20px;
}

.phub-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 10px;
    background: var(--highlight-glow);
    color: var(--highlight);
    font-size: 0.9rem;
}

.phub-card-title {
    margin: 0;
    font-size: var(--fs-600);
    font-weight: var(--fw-bold);
    color: var(--text-primary);
}

.phub-card-sub {
    margin: 2px 0 0;
    font-size: var(--fs-400);
    color: var(--text-muted);
}

.phub-card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 24px;
    padding-top: 20px;
    border-top: 1px solid var(--border-default);
}

/* One button to a row, full width - the same shape as the delete in the
   danger zone underneath, so the two read as a pair of block actions. */
.phub-card-actions .btn-portal-primary {
    flex: 1 1 100%;
    width: 100%;
}

/* ─── Banner editor ─── */
.phub-banner-edit {
    position: relative;
    height: 190px;
    margin-bottom: 22px;
    border-radius: 12px;
    border: 1px solid var(--border-default);
    /* The same gradient the header carries, in case the picture above it ever
       fails to load. */
    background:
    radial-gradient(120% 140% at 15% 0%, var(--highlight-glow), transparent 60%),
    linear-gradient(120deg, rgba(13, 148, 136, 0.35), rgba(30, 41, 59, 0.55));
    background-size: cover;
    background-position: center;
}

/* Along the bottom edge, out of the picture's own subject. Pinned left as well
   as right so a narrow card wraps the pair instead of pushing them off it. */
.phub-banner-edit-actions {
    position: absolute;
    right: 12px;
    bottom: 12px;
    left: 12px;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
}


.phub-avatar-row {
    display: flex;
    align-items: center;
    gap: 18px;
}

.phub-avatar-thumb {
    width: 92px;
    height: 92px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--border-default);
    background: var(--surface-overlay);
}

/* ─── Buttons and fields ─── */
.phub-ghost-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 16px;
    border: 1px solid var(--border-default);
    border-radius: 10px;
    background: var(--surface-raised);
    color: var(--text-primary);
    font-size: var(--fs-400);
    font-weight: var(--fw-semibold);
    cursor: pointer;
    transition: border-color 0.18s ease, color 0.18s ease;
}

.phub-ghost-btn:hover:not(:disabled) {
    border-color: var(--highlight);
    color: var(--highlight);
}

.phub-ghost-btn:disabled {
    opacity: 0.6;
    cursor: default;
}

.phub-ghost-btn-danger:hover:not(:disabled) {
    border-color: var(--text-danger);
    color: var(--text-danger);
}

.phub-field {
    margin-bottom: 20px;
}

.phub-field:last-of-type {
    margin-bottom: 0;
}

.phub-field-label {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    color: var(--text-primary);
    font-size: var(--fs-500);
    font-weight: var(--fw-semibold);
}

.phub-field-label i {
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* The lock sits inside the field to say "read-only" without a second line. */
.phub-locked-field {
    position: relative;
}

.phub-locked-field i {
    position: absolute;
    top: 50%;
    right: 14px;
    transform: translateY(-50%);
    color: var(--text-muted);
    font-size: 0.8rem;
    pointer-events: none;
}

.phub-locked-field .form-control {
    padding-right: 38px;
}


/* The closed box is left to .form-control, so Relationship Status matches
   every other field. Only the list that drops open is set here: it is a native
   popup rather than part of the box, so it inherits nothing from it and would
   otherwise fall back to the OS colours. */
#phubRel option {
    background: var(--surface-raised);
    color: var(--text-primary);
}

.phub-hint {
    margin: 8px 0 0;
    color: var(--text-muted);
    font-size: var(--fs-300);
}

/* Preference switches: a titled row with its explanation, the control parked
   on the right. The master row is tinted so the pair beneath reads as
   answering to it. */
.phub-switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    margin-bottom: 12px;
    padding: 16px 20px;
    border-radius: 14px;
    border: 1px solid var(--border-default);
    background: var(--surface-overlay);
}


.phub-switch-row-lead {
    margin-bottom: 22px;
    border-color: var(--highlight);
    background: var(--highlight-glow);
}


.phub-switch-copy h4 {
    margin: 0;
    font-size: var(--fs-500);
    font-weight: var(--fw-bold);
    color: var(--text-primary);
}


.phub-switch-copy p {
    margin: 4px 0 0;
    max-width: 52ch;
    color: var(--text-muted);
    font-size: var(--fs-300);
}


/* What the browser said last time the notification switch was touched. Empty
   until then, so it takes no room on a first visit. */
.phub-switch-status:empty {
    display: none;
}


.phub-switch-status {
    color: var(--highlight);
}


/* Bootstrap's switch, sized up. The off knob is left to Bootstrap, which
   already picks one per data-bs-theme - near-black against a light page,
   25% white against a dark one. Painting it here meant the white knob was
   used on every theme, and on the light one that is a white dot on an
   almost-white track: a switch with nothing in it. */
.phub-switch-row .form-check-input {
    width: 3rem;
    height: 1.6rem;
    margin: 0;
    flex-shrink: 0;
    cursor: pointer;
    background-color: var(--surface-inset);
    border-color: var(--border-default);
}


/* Bootstrap's own switch blue, written out rather than tokenised: the
   account dropdown's auto-download switch is a bare .form-check-input and so
   wears this exact colour in every theme, and these have to agree with it.
   The accent cannot do that job - two of the themes make it near-black or
   white, which reads as a switch that is off. */
.phub-switch-row .form-check-input:checked {
    background-color: #0d6efd;
    border-color: #0d6efd;
    /* The knob has to be restated here. The rule above paints a 25%-white
       one so it shows on a dark row, and it outranks the solid white knob
       Bootstrap swaps in on :checked - which left the switch a plain blue
       pill with nothing in it to read as a knob. */
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}


/* Stands in for the section rows while the profile is private. */
.phub-switch-empty {
    padding: 44px 24px;
    border-radius: 14px;
    border: 1px solid var(--border-default);
    background: var(--surface-overlay);
    text-align: center;
    color: var(--text-muted);
}


.phub-switch-empty i {
    font-size: 2rem;
    opacity: 0.35;
}


.phub-switch-empty p {
    margin: 14px auto 0;
    max-width: 44ch;
    font-size: var(--fs-400);
}






/* Danger zone: fenced off below the card's own actions by a rule and a
   label, so the delete cannot be mistaken for one more setting. */
.phub-danger-zone {
    margin-top: 28px;
    padding-top: 22px;
    border-top: 1px solid var(--border-default);
    text-align: center;
}


.phub-danger-label {
    display: block;
    margin-bottom: 14px;
    color: var(--text-muted);
    font-size: var(--fs-400);
}


/* Outlined rather than filled: it should read as available, not inviting. */
.phub-danger-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    padding: 12px 18px;
    border: 1px solid var(--danger-500, #ef4444);
    border-radius: 12px;
    background: transparent;
    color: var(--danger-500, #ef4444);
    font-size: var(--fs-500);
    font-weight: var(--fw-bold);
    cursor: pointer;
    transition: background 0.18s ease, color 0.18s ease;
}


.phub-danger-btn:hover:not(:disabled),
.phub-danger-btn:focus-visible {
    background: var(--danger-500, #ef4444);
    color: #fff;
}


.phub-danger-btn:disabled {
    opacity: 0.6;
    cursor: progress;
}


/* A rule the server enforces, called out in a tinted box so it reads as a
   constraint rather than a suggestion. */
.phub-note {
    position: relative;
    margin: 10px 0 0;
    padding: 12px 14px 12px 28px;
    border: 1px solid var(--border-default);
    border-radius: 10px;
    background: var(--surface-overlay);
    color: var(--text-muted);
    font-size: var(--fs-300);
    line-height: var(--lh-normal);
}

.phub-note::before {
    content: "•";
    position: absolute;
    left: 14px;
    color: var(--highlight);
    font-size: 1rem;
    line-height: var(--lh-snug);
}

.phub-note-info::before {
    color: var(--accent-secondary);
}


.phub-address {
    color: var(--text-primary);
    font-weight: var(--fw-semibold);
}

/* ─── Verified badge and the Follow action ─────────────────────────
   Added beside the identity block in the header; the avatar and name
   layout above is unchanged.
   ----------------------------------------------------------------- */
/* The seal itself is .verified-seal in main.css, shared with the comment
   record and with the blog and gallery discussions. This is only the span it
   sits in, which has to stop the flex row from stretching it. */
.phub-verified {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}

/* Pushes the actions to the far end of the identity row, opposite the
   avatar and name. */
.phub-identity-actions {
    position: absolute;
    right: 24px;
    bottom: 24px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    flex-wrap: wrap;
}

.phub-follow-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 18px;
    border-radius: 10px;
    border: 1px solid transparent;
    background: var(--highlight);
    /* Not #000: --highlight is near-black in light mode and white in black
       mode, so the label has to be whatever that theme puts on top of it. The
       icon inherits this. */
    color: var(--text-on-accent);
    font-size: var(--fs-400);
    font-weight: var(--fw-bold);
    text-decoration: none;
    white-space: nowrap;
    transition: filter 0.18s ease;
}

.phub-follow-btn:hover {
    filter: brightness(1.08);
    color: var(--text-on-accent);
}


.phub-banner-badge {
    position: absolute;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: calc(100% - 24px);
    padding: 6px 12px;
    border-radius: 10px;
    border: 1px solid var(--border-default);
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    color: #fff;
    font-size: 0.78rem;
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


/* Relationship status as a chat bubble above the avatar, its tail coming down
   on the crown of the circle. The card and its arrow are Bootstrap's .popover,
   so all this rule does is retune the component's variables to the site's
   theme and park it against the avatar - Popper normally places both, and
   there is no JS behind this one. */
.phub-avatar-cloud.popover {
    /* Bootstrap's own API, so the card, the arrow and their borders stay in
       step with each other. Border width is 0px rather than 0 because
       Bootstrap subtracts it inside a calc(), where a unitless zero voids the
       whole expression and drops the arrow back into the card. */
    --bs-popover-zindex: 3;
    --bs-popover-max-width: 240px;
    --bs-popover-border-width: 0px;
    --bs-popover-border-radius: 20px;
    /* Painted below by the two layers, so Bootstrap must not paint over them. */
    --bs-popover-bg: transparent;
    --bs-popover-body-color: var(--n-900);
    --bs-popover-body-padding-x: 18px;
    --bs-popover-body-padding-y: 10px;
    --bs-popover-font-size: var(--fs-400);
    /* Long enough to cross the gap and land on the top of the circle. */
    --bs-popover-arrow-width: 1.25rem;
    --bs-popover-arrow-height: 1.5rem;
    --bs-popover-arrow-border: transparent;

    /* Popper normally supplies the popover's position inline; without it the
       card places itself. Pinned by its left corner, offset so the arrow
       hanging off that corner comes down on the avatar's midline - the crown
       of the circle - whatever the status says. The 28px is the arrow's own
       inset plus half its width; the card grows rightwards from there. */
    position: absolute;
    top: auto;
    right: auto;
    left: calc(50% - 28px);
    bottom: calc(100% + 19px);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    /* Inside the owner's picker button, so it must not swallow the click. */
    pointer-events: none;

    /* Fixed white with near-black type, in every theme. It hangs over the
       banner photograph rather than over a theme surface, so it is chrome on a
       picture - the same reasoning that lets the --brand-* colours and the
       banner buttons' glass be written directly rather than tokenised. */
    background: var(--n-0);
}

/* Popper would place the arrow too. Sat in the card's bottom-left corner,
   where it reaches down onto the top of the circle; Bootstrap draws the
   triangle itself. Bootstrap hangs the arrow exactly off the card's bottom
   edge, which leaves a sliver of daylight where the rounded corner curves
   away from the arrow's flank - pulling it 4px up into the card closes that
   seam, and the triangle is that much longer to keep its tip on the circle. */
/* Clipped box rather than Bootstrap's two border-triangles (dropped below), so
   the tail is the same white as the card. A 1px overlap hides the seam where
   the two meet; both are opaque, so it cannot tint twice. */
.phub-avatar-cloud .popover-arrow {
    position: absolute;
    right: auto;
    left: 18px;
    bottom: calc(1px - var(--bs-popover-arrow-height));
    width: var(--bs-popover-arrow-width);
    height: var(--bs-popover-arrow-height);
    background: var(--n-0);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
}


.phub-avatar-cloud.popover .popover-arrow::before,
.phub-avatar-cloud.popover .popover-arrow::after {
    display: none;
}


/* ── Photo-overlay glass ──────────────────────────────────────────
   Chrome that sits on a picture rather than on a theme surface: the share and
   settings buttons on the header banner, and the banner editor's own buttons
   in Settings. One rule for both, the way main.css lists every panel that
   takes the wallpaper glass.

   Barely a fill: the blur is what makes it a pane, and the tint only has to
   hold the label legible over a bright patch of the picture. The picture
   underneath is the same in every theme, so these values are fixed rather than
   tokenised - the same reasoning that lets the --brand-* colours be used
   directly. Retune the treatment here and both follow. */
.phub-icon-btn,
.phub-banner-edit-actions .phub-ghost-btn {
    border: 1px solid rgba(255, 255, 255, 0.28);
    background: rgba(0, 0, 0, 0.18);
    backdrop-filter: blur(14px) saturate(160%);
    -webkit-backdrop-filter: blur(14px) saturate(160%);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18),
                0 4px 16px rgba(0, 0, 0, 0.28);
    color: var(--n-0);
    /* The tint is thin enough that a light banner could wash the label out, so
       the type carries its own shadow rather than the pane going darker. */
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
    transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}


/* Change banner hovers the way Change Avatar does - the label and its glyph
   take the accent - but the box comes with them. Over a photograph a border
   and a text colour alone barely register, so the pane itself recolours.

   The fill stays translucent, so it is still glass rather than a solid button,
   and the label takes --text-on-accent rather than --highlight: --highlight is
   near-black on the light theme and would disappear into its own fill. */
.phub-banner-edit-actions .phub-ghost-btn:hover:not(:disabled) {
    border-color: var(--highlight);
    /* First declaration is the fallback for browsers without color-mix. */
    background: var(--highlight);
    background: color-mix(in srgb, var(--highlight) 55%, transparent);
    color: var(--text-on-accent);
    text-shadow: none;
}


/* Remove is the same move in red: the border goes to it and the box follows.
   White label rather than --text-on-accent, because the fill here is the
   danger red in every theme, not the theme's own accent. */
.phub-banner-edit-actions .phub-ghost-btn-danger:hover:not(:disabled) {
    border-color: var(--text-danger);
    background: var(--text-danger);
    background: color-mix(in srgb, var(--text-danger) 55%, transparent);
    color: var(--n-0);
}


/* Bio under the handle, shown in full however long it runs. Capped at 70ch
   for readability and wrapping long words so a pasted URL cannot stretch the
   header sideways. */
.phub-bio {
    /* Same right inset .phub-names carries: the Follow button is parked in that
       corner, and the bio is no longer inside the block that was reserving it. */
    margin: 6px 160px 0 0;
    max-width: 70ch;
    color: var(--text-muted);
    font-size: var(--fs-400);
    line-height: var(--lh-relaxed);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* Character count under the bio field. */
.phub-counter {
    margin: 6px 0 0;
    text-align: right;
    color: var(--text-muted);
    font-size: var(--fs-200);
    font-variant-numeric: tabular-nums;
}

.phub-counter.is-full {
    color: var(--highlight);
    font-weight: var(--fw-semibold);
}


/* ─── Lightbox ─────────────────────────────────────────────────────
   A photo in the Profile tab opens over the page instead of navigating to the
   image file. Twin of the one in blog.css, which this page does not load; see
   the note at the top of this file about the other twins.
   ────────────────────────────────────────────────────────────────── */
#image-lightbox {
    position: fixed;
    inset: 0;
    z-index: 10000;
    display: none;
    align-items: center;
    justify-content: center;
    /* A scrim over a photograph, not a theme surface, so it is written out
       rather than tokenised - the same call the banner chrome makes. */
    background: rgba(0, 0, 0, 0.92);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    cursor: zoom-out;
}


#image-lightbox.open {
    display: flex;
}


/* Column, so the counter sits under the picture rather than over it. The
   picture takes what is left after the caption, which is what min-height: 0
   is for - a flex item will not shrink below its content without it, and an
   image's content is its natural size. */
.lightbox-figure {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    max-width: 95%;
    max-height: 95%;
    margin: 0;
}


#lightbox-img {
    min-height: 0;
    max-width: 100%;
    object-fit: contain;
    border-radius: 4px;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
    transform: scale(0.9);
    transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}


#image-lightbox.open #lightbox-img {
    transform: scale(1);
}


/* "3 / 8". Tabular figures so the line does not jog sideways as the number
   changes width. */
.lightbox-count {
    flex: 0 0 auto;
    color: rgba(255, 255, 255, 0.75);
    font-size: var(--fs-400);
    font-variant-numeric: tabular-nums;
}


/* Chrome over the picture, so the same written-out white glass the banner
   buttons use rather than a theme surface. */
.lightbox-btn {
    position: absolute;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(14px) saturate(160%);
    -webkit-backdrop-filter: blur(14px) saturate(160%);
    color: var(--n-0);
    font-size: var(--fs-600);
    /* The overlay is zoom-out; these are not. */
    cursor: pointer;
    transition: background 0.18s ease, border-color 0.18s ease;
}


.lightbox-btn:hover {
    background: rgba(255, 255, 255, 0.22);
    border-color: rgba(255, 255, 255, 0.5);
}


/* display: flex would otherwise beat the attribute on a one-photo gallery. */
.lightbox-btn[hidden] {
    display: none;
}


.lightbox-close {
    top: 18px;
    right: 18px;
}


.lightbox-prev,
.lightbox-next {
    top: 50%;
    transform: translateY(-50%);
}


.lightbox-prev {
    left: 18px;
}


.lightbox-next {
    right: 18px;
}


/* The picture is the control, so it says so. Only in the Photos pane: a
   Views or Likes tile is a post and goes to the post. */
.hist-photo-grid .hist-tile {
    cursor: zoom-in;
}


/* ==========================================================================
   9.  RESPONSIVE — TABLET AND BELOW (≤ 768px)
   ==========================================================================
   Where the header gives way: the banner shortens and the rails start
   scrolling. The avatar deliberately does not shrink — see section 5. */

@media (max-width: 768px) {

    #user-portal .container {
        padding-top: 2rem !important;
    }

    .modal-dialog {
        max-width: 95%;
        margin: auto !important;
    }

    /* The history row tightens up: a phone has less width to give the
       sentence, and none to spare on padding. */
    .acct-history-row {
        gap: 11px;
        padding: 12px;
    }


    .acct-history-when {
        align-self: flex-start;
        font-size: var(--fs-200);
    }

    .phub-tabs,
    .phub-subtabs {
        gap: 6px;
        padding: 5px;
        flex-wrap: nowrap;
        -webkit-overflow-scrolling: touch;
    }

    .phub-tab,
    .phub-subtab {
        /* Grow to share the rail, never shrink below the label. */
        flex: 1 0 auto;
        gap: 6px;
        padding: 8px 12px;
        font-size: var(--fs-300);
    }

    .phub-tab i {
        font-size: 0.78rem;
    }

    /* Parity with My Circle is kept at both sizes: 180px where there is room,
       140px here, where that circle would be most of the card's width.
       home.css steps its own circles down at this same breakpoint. The pull-up
       is half the height, as it is at every size, so the avatar goes on
       straddling the banner's lower edge. */
    .phub-avatar img {
        width: 140px;
        height: 140px;
    }

    .phub-avatar {
        margin-top: -70px;
    }

}


/* ==========================================================================
   10. RESPONSIVE — NARROW (≤ 576px)
   ==========================================================================
   The header goes full bleed and its identity block re-flows; the two-column
   rows in Settings stack. */

@media (max-width: 576px) {

    /* The banner starts directly under the topbar. That bar is fixed and
       reserves no height of its own, so this padding IS the clearance for it -
       and three boxes were each adding their own (the page container, the
       portal's container inside it, and the column), which put the header
       ~26px below where the bar ends. One of them pays it now, and it pays
       exactly #topbar's own min-height, so there is nothing left over. */
    .container:has(#user-portal) {
        padding-top: 70px !important;
    }

    #user-portal .container {
        padding-top: 0 !important;
    }

    /* The arrows stop flanking the picture and sit under it, beside the
       counter - at this width they would otherwise be on top of the photo. */
    .lightbox-prev,
    .lightbox-next {
        top: auto;
        bottom: 18px;
        transform: none;
    }

    .lightbox-figure {
        max-height: 78%;
    }

    /* Full bleed, for the whole page rather than the header alone. The column's
       16px gutter goes, so every block in it reaches both edges: the header,
       the tab rails and the panes are all one width. Each of them carries
       padding of its own, so nothing inside ends up against the glass. */
    .profile-content {
        padding-top: 0;
        padding-left: 0;
        padding-right: 0;
    }

    /* A rounded corner is a corner you can see clear of - at the screen's edge
       there is nothing to clear, so the radius and the side borders go and
       these read as full-width bands. The top and bottom edges stay: those are
       what still separates one band from the next. */
    .phub-header,
    .phub-tabs,
    .phub-subtabs,
    .phub-card,
    .profile-hero-card {
        border-radius: 0;
        border-left: 0;
        border-right: 0;
    }

    /* The desktop arrangement at phone width: the avatar on its own line with
       the Follow button opposite it, then the name, handle and bio stacked
       underneath. The DOM order stays name -> bio -> action, which is the order
       they should be read in; `order` only moves the action up into the
       avatar's line, where the desktop layout also puts it. */
    .phub-identity {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        column-gap: 14px;
        padding-left: 16px;
        padding-right: 16px;
    }

    .phub-avatar {
        order: 1;
        flex: 0 0 auto;
    }

    .phub-names {
        order: 3;
        flex: 1 0 100%;
        /* Without this a long name refuses to wrap and pushes the row wider
           than the card. */
        min-width: 0;
        padding: 14px 0 0;
    }

    .phub-bio {
        order: 4;
        flex: 1 0 100%;
        margin-right: 0;
    }

    .phub-name {
        font-size: var(--fs-900);
    }

    /* Too narrow for copy and control side by side. */
    .phub-switch-row {
        align-items: flex-start;
        flex-direction: column;
        gap: 12px;
    }

    .phub-avatar-row {
        flex-direction: column;
        align-items: flex-start;
    }

    .phub-card-actions .btn-portal-primary,
    .phub-card-actions .phub-ghost-btn {
        flex: 1 1 auto;
        justify-content: center;
    }

    /* Beside the avatar, at the far end of its line - the same corner the
       absolute positioning puts it in at wider sizes, but in the flow, so the
       name and bio below it cannot end up underneath it. */
    .phub-identity-actions {
        order: 2;
        position: static;
        flex: 1 1 auto;
        margin: 0 0 6px;
        justify-content: flex-end;
    }

    .phub-banner-badge {
        font-size: 0.72rem;
        padding: 5px 10px;
    }
}


/* ==========================================================================
   11. RESPONSIVE — PHONE (≤ 470px)
   ==========================================================================
   main.css's own phone breakpoint. Only the rails need a second step down. */

@media (max-width: 470px) {

    .phub-tabs,
    .phub-subtabs {
        gap: 4px;
        padding: 4px;
    }

    .phub-tab,
    .phub-subtab {
        gap: 5px;
        padding: 7px 9px;
        font-size: var(--fs-200);
    }
}


/* ==========================================================================
   12. RESPONSIVE — FROM TABLET UP (≥ 768px)
   ==========================================================================
   Roomier padding inside the settings cards. */

@media (min-width: 768px) {

    .phub-card {
        padding: 30px;
    }
}


/* ==========================================================================
   13. RESPONSIVE — DESKTOP (≥ 1024px)
   ==========================================================================
   The sidebar is permanent here, so the column shifts right to line up with
   the footer and every other page. */

@media (min-width: 1024px) {

    /* Same fix the home page uses: the global .container adds padding-right: 10rem
       on desktop, which pushes the portal column left of centre. Cancel it so
       #user-portal .container (margin auto, 1100px) centres in the visible area
       between the sidebar and the right edge — identical to the gallery section. */
    .container:has(#user-portal) {
        padding-right: 0 !important;
    }


    /* .container stays full-bleed here, as it does on every other page: it
       paints the page surface, so constraining IT would shrink that surface
       into a visible panel and let the wallpaper leak around it. The column
       lives on .profile-content below instead. */
    #user-portal .container {
        padding-left: 0 !important;
        padding-right: 0 !important;
        padding-top: 2rem !important;
    }

    .profile-content {
        left: 4rem;
    }
}


/* ==========================================================================
   14. REDUCED MOTION
   ==========================================================================
   The two transitions worth cancelling: the tab pill and the avatar fade. */

@media (prefers-reduced-motion: reduce) {

    .phub-avatar-photo + .phub-avatar-alt {
        transition: none;
    }

    .phub-tab-indicator {
        transition: none;
    }

    #lightbox-img {
        transition: none;
    }
}
