diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 13bba06d425bbf111d9417c949d613da9d0a28c9..abc72018c82c845ad0808299a9972e21abe1409b 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -72,7 +72,7 @@ export default class ActivityCalendar {
     this.clickDay = this.clickDay.bind(this);
     this.currentSelectedDate = '';
     this.daySpace = 1;
-    this.daySize = 15;
+    this.daySize = 14;
     this.daySizeWithSpace = this.daySize + this.daySpace * 2;
     this.monthNames = [
       __('Jan'),
@@ -131,7 +131,6 @@ export default class ActivityCalendar {
     this.renderDays();
     this.renderMonths();
     this.renderDayTitles();
-    this.renderKey();
   }
 
   // Add extra padding for the last month label if it is also the last column
@@ -153,7 +152,7 @@ export default class ActivityCalendar {
       .select(container)
       .append('svg')
       .attr('width', width)
-      .attr('height', 169)
+      .attr('height', 140)
       .attr('class', 'contrib-calendar')
       .attr('data-testid', 'contrib-calendar');
   }
@@ -257,25 +256,6 @@ export default class ActivityCalendar {
       .text((date) => this.monthNames[date.month]);
   }
 
-  renderKey() {
-    this.svg
-      .append('g')
-      .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
-      .selectAll('rect')
-      .data(CONTRIB_LEGENDS)
-      .enter()
-      .append('rect')
-      .attr('width', this.daySize)
-      .attr('height', this.daySize)
-      .attr('x', (_, i) => this.daySizeWithSpace * i)
-      .attr('y', 0)
-      .attr('data-level', (_, i) => i)
-      .attr('class', 'user-contrib-cell has-tooltip contrib-legend')
-      .attr('title', (x) => x.title)
-      .attr('data-container', 'body')
-      .attr('data-html', true);
-  }
-
   clickDay(stamp) {
     if (this.currentSelectedDate !== stamp.date) {
       this.currentSelectedDate = stamp.date;
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index f9e22808b0d9c74d0182e026f71e44a032c29127..35d7edad96b8bae05ffc250ca9efed4bcb408467 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,8 +1,8 @@
 // TODO: Remove this with the removal of the old navigation.
 // See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
 
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
 import $ from 'jquery';
+import initReadMore from '~/read_more';
 import Activities from '~/activities';
 import AjaxCache from '~/lib/utils/ajax_cache';
 import axios from '~/lib/utils/axios_utils';
@@ -66,18 +66,35 @@ import UserOverviewBlock from './user_overview_block';
 
 const CALENDAR_TEMPLATE = `
   <div class="calendar">
-    <div class="js-contrib-calendar"></div>
-    <div class="calendar-hint"></div>
+    <div class="js-contrib-calendar gl-overflow-x-auto"></div>
+    <div class="calendar-help gl-display-flex gl-justify-content-space-between gl-ml-auto gl-mr-auto">
+      <div class="calendar-legend">
+        <svg width="80px" height="20px">
+          <g>
+            <rect width="13" height="13" x="2" y="2" data-level="0" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
+              'No contributions',
+            )}" data-container="body"></rect>
+            <rect width="13" height="13" x="17" y="2" data-level="1" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
+              '1-9 contributions',
+            )}" data-container="body"></rect>
+            <rect width="13" height="13" x="32" y="2" data-level="2" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
+              '10-19 contributions',
+            )}" data-container="body"></rect>
+            <rect width="13" height="13" x="47" y="2" data-level="3" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
+              '20-29 contributions',
+            )}" data-container="body"></rect>
+            <rect width="13" height="13" x="62" y="2" data-level="4" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
+              '30+ contributions',
+            )}" data-container="body"></rect>
+          </g>
+        </svg>
+      </div>
+      <div class="calendar-hint gl-font-sm gl-text-secondary"></div>
+    </div>
   </div>
 `;
 
-const CALENDAR_PERIOD_6_MONTHS = 6;
 const CALENDAR_PERIOD_12_MONTHS = 12;
-/* computation based on
- * width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
- * (see activity_calendar.js)
- */
-const OVERVIEW_CALENDAR_BREAKPOINT = 918;
 
 export default class UserTabs {
   constructor({ defaultAction, action, parentEl }) {
@@ -105,12 +122,6 @@ export default class UserTabs {
       .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
       .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event))
       .on('click', '.gl-pagination a', (event) => this.changeProjectsPage(event));
-
-    window.addEventListener('resize', () => this.onResize());
-  }
-
-  onResize() {
-    this.loadActivityCalendar();
   }
 
   changeProjectsPage(e) {
@@ -194,19 +205,25 @@ export default class UserTabs {
       return;
     }
 
+    initReadMore();
+
     this.loadActivityCalendar();
 
     UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
       requestParams: { limit: 15 },
     });
+
     UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
-      requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
+      requestParams: { limit: 3, skip_pagination: true, skip_namespace: true, card_mode: true },
     });
 
     this.loaded.overview = true;
   }
 
   static renderMostRecentBlocks(container, options) {
+    if ($(container).length === 0) {
+      return;
+    }
     // eslint-disable-next-line no-new
     new UserOverviewBlock({
       container,
@@ -218,8 +235,6 @@ export default class UserTabs {
 
   loadActivityCalendar() {
     const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
-    if (!$calendarWrap.length || bp.getBreakpointSize() === 'xs') return;
-
     const calendarPath = $calendarWrap.data('calendarPath');
 
     AjaxCache.retrieve(calendarPath)
@@ -240,7 +255,6 @@ export default class UserTabs {
   }
 
   static renderActivityCalendar(data, $calendarWrap) {
-    const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap);
     const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
     const utcOffset = $calendarWrap.data('utcOffset');
     const calendarHint = __('Issues, merge requests, pushes, and comments.');
@@ -257,8 +271,12 @@ export default class UserTabs {
       calendarActivitiesPath,
       utcOffset,
       firstDayOfWeek: gon.first_day_of_week,
-      monthsAgo,
+      CALENDAR_PERIOD_12_MONTHS,
     });
+
+    // Scroll to end
+    const calendarContainer = document.querySelector('.js-contrib-calendar');
+    calendarContainer.scrollLeft = calendarContainer.scrollWidth;
   }
 
   toggleLoading(status) {
@@ -282,11 +300,4 @@ export default class UserTabs {
   getCurrentAction() {
     return this.$parentEl.find('.nav-links a.active').data('action');
   }
-
-  static getVisibleCalendarPeriod($calendarWrap) {
-    const width = $calendarWrap.width();
-    return width < OVERVIEW_CALENDAR_BREAKPOINT
-      ? CALENDAR_PERIOD_6_MONTHS
-      : CALENDAR_PERIOD_12_MONTHS;
-  }
 }
diff --git a/app/assets/javascripts/profile/components/activity_calendar.vue b/app/assets/javascripts/profile/components/activity_calendar.vue
index d359b478d35c0523851b6549f0c947e233a86301..b9668210f79158a0ed7801dde1519cb1468daa40 100644
--- a/app/assets/javascripts/profile/components/activity_calendar.vue
+++ b/app/assets/javascripts/profile/components/activity_calendar.vue
@@ -1,12 +1,8 @@
 <script>
 import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { debounce } from 'lodash';
-
 import { __ } from '~/locale';
 import AjaxCache from '~/lib/utils/ajax_cache';
 import ActivityCalendar from '~/pages/users/activity_calendar';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
 import { getVisibleCalendarPeriod } from '../utils';
 
 export default {
@@ -20,26 +16,14 @@ export default {
   data() {
     return {
       isLoading: true,
-      showCalendar: true,
       hasError: false,
     };
   },
   mounted() {
     this.renderActivityCalendar();
-    window.addEventListener('resize', this.handleResize);
-  },
-  beforeDestroy() {
-    window.removeEventListener('resize', this.handleResize);
   },
   methods: {
     async renderActivityCalendar() {
-      if (bp.getBreakpointSize() === 'xs') {
-        this.showCalendar = false;
-
-        return;
-      }
-
-      this.showCalendar = true;
       this.isLoading = true;
       this.hasError = false;
 
@@ -66,9 +50,6 @@ export default {
         this.hasError = true;
       }
     },
-    handleResize: debounce(function debouncedHandleResize() {
-      this.renderActivityCalendar();
-    }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
     handleClickDay() {
       // Render activities for specific day.
       // Blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/378695
@@ -78,8 +59,8 @@ export default {
 </script>
 
 <template>
-  <div v-if="showCalendar" ref="calendarContainer">
-    <gl-loading-icon v-if="isLoading" size="md" />
+  <div ref="calendarContainer" class="gl-pb-5 gl-border-b">
+    <gl-loading-icon v-if="isLoading" size="sm" />
     <gl-alert
       v-else-if="hasError"
       :title="$options.i18n.errorAlertTitle"
@@ -88,13 +69,11 @@ export default {
       :primary-button-text="$options.i18n.retry"
       @primaryAction="renderActivityCalendar"
     />
-    <div v-else class="gl-text-center">
-      <div class="gl-display-inline-block gl-relative">
-        <div ref="calendarSvgContainer"></div>
-        <p class="gl-absolute gl-right-0 gl-bottom-0 gl-mb-0 gl-font-sm">
-          {{ $options.i18n.calendarHint }}
-        </p>
-      </div>
+    <div v-else class="gl-display-inline-block gl-relative gl-w-full">
+      <div ref="calendarSvgContainer"></div>
+      <p class="gl-absolute gl-right-0 gl-bottom-0 gl-mb-0 gl-font-sm gl-text-secondary">
+        {{ $options.i18n.calendarHint }}
+      </p>
     </div>
   </div>
 </template>
diff --git a/app/assets/javascripts/profile/components/overview_tab.vue b/app/assets/javascripts/profile/components/overview_tab.vue
index 8cfa3fb3eea188ed45158038df5dacf477b274ec..ab8a2871a41c1c78214ddf418cd9b42137bd7ce9 100644
--- a/app/assets/javascripts/profile/components/overview_tab.vue
+++ b/app/assets/javascripts/profile/components/overview_tab.vue
@@ -54,19 +54,19 @@ export default {
 
 <template>
   <gl-tab :title="$options.i18n.title">
-    <activity-calendar />
-    <div class="gl-mx-n5 gl-display-flex gl-flex-wrap">
-      <div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="activity-section">
+    <div class="gl-mt-5 gl-display-flex gl-flex-wrap">
+      <div class="gl-w-full" data-testid="activity-section">
         <div
           class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
         >
           <h4 class="gl-flex-grow-1">{{ $options.i18n.activity }}</h4>
           <gl-link href="">{{ $options.i18n.viewAll }}</gl-link>
         </div>
+        <activity-calendar />
         <gl-loading-icon v-if="eventsLoading" class="gl-mt-5" size="md" />
         <contribution-events v-else :events="events" />
       </div>
-      <div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="personal-projects-section">
+      <div class="gl-w-full" data-testid="personal-projects-section">
         <div
           class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
         >
diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue
index e24167eb4fa713defd1aec55ef1791a64495357a..a15794ac07d517454862c2b0efa53dbf364f3d16 100644
--- a/app/assets/javascripts/profile/components/profile_tabs.vue
+++ b/app/assets/javascripts/profile/components/profile_tabs.vue
@@ -81,7 +81,7 @@ export default {
   },
   async mounted() {
     try {
-      const response = await getUserProjects(this.userId, { per_page: 10 });
+      const response = await getUserProjects(this.userId, { per_page: 3 });
       this.personalProjects = convertObjectPropsToCamelCase(response.data, { deep: true }).map(
         (project) => {
           // This API does not return the `visibility` key if user is signed out.
diff --git a/app/assets/javascripts/profile/components/user_achievements.vue b/app/assets/javascripts/profile/components/user_achievements.vue
index 7ce6b61c4aca7ca73334202aeba0d051e7ead561..f317f7871f83fc959c106dcf3662bbccce961f0f 100644
--- a/app/assets/javascripts/profile/components/user_achievements.vue
+++ b/app/assets/javascripts/profile/components/user_achievements.vue
@@ -28,6 +28,11 @@ export default {
       },
     },
   },
+  computed: {
+    hasUserAchievements() {
+      return Boolean(this.userAchievements?.length);
+    },
+  },
   methods: {
     processNodes(nodes) {
       return Object.entries(groupBy(nodes, 'achievement.id'))
@@ -67,12 +72,16 @@ export default {
   i18n: {
     awardedBy: s__('Achievements|Awarded %{timeAgo} by %{namespace}'),
     awardedByUnknownNamespace: s__('Achievements|Awarded %{timeAgo} by a private namespace'),
+    achievementsLabel: s__('Achievements|Achievements'),
   },
 };
 </script>
 
 <template>
-  <div class="gl-mb-3">
+  <div v-if="hasUserAchievements">
+    <h2 class="gl-font-base gl-mb-2 gl-mt-4">
+      {{ $options.i18n.achievementsLabel }}
+    </h2>
     <div
       v-for="userAchievement in userAchievements"
       :key="userAchievement.id"
@@ -85,7 +94,7 @@ export default {
         :size="32"
         tabindex="0"
         shape="rect"
-        class="gl-mx-2 gl-p-1 gl-border-none"
+        class="gl-mr-2 gl-p-1 gl-border-none"
       />
       <br />
       <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 912f0145bf1dcf4653d51db9c16aa06cb479be5b..561a7d9f4cb25701bf32e9fea35a693ccc05dbb6 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -29,97 +29,16 @@
   }
 }
 
-.calendar-block {
-  padding-left: 0;
-  padding-right: 0;
-  border-top: 0;
-
-  @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
-    overflow-x: auto;
-  }
-}
-
-.calendar-hint {
-  font-size: 12px;
-  direction: ltr;
-  margin-top: -23px;
-  float: right;
+.calendar-help {
+  // Match width of calendar
+  max-width: 864px;
 }
 
-.cover-block {
-  text-align: center;
-  background: var(--gray-50, $gray-light);
-  padding-top: 44px;
-  position: relative;
-
-  .avatar-holder {
-    .avatar,
-    .identicon {
-      margin: 0 auto;
-      float: none;
-    }
-
-    .identicon {
-      border-radius: 50%;
-    }
-  }
-
-  .cover-title {
-    color: var(--gl-text-color, $gl-text-color);
-    font-size: 23px;
-
-    h1 {
-      color: var(--gl-text-color, $gl-text-color);
-      margin-bottom: 6px;
-      font-size: 23px;
-    }
-
-    .visibility-icon {
-      display: inline-block;
-      margin-left: 5px;
-      font-size: 18px;
-      color: color('gray');
-    }
-
-    p {
-      padding: 0 $gl-padding;
-      color: var(--gl-text-color, $gl-text-color);
-    }
-  }
-
-  .cover-controls {
-    @include media-breakpoint-up(sm) {
-      position: absolute;
-      top: 1rem;
-      right: 1.25rem;
-    }
-
-    &.left {
-      @include media-breakpoint-up(sm) {
-        left: 1.25rem;
-        right: auto;
-      }
-    }
-  }
-
-  &.user-cover-block {
-    padding: 24px 0 0;
-
-    .nav-links {
-      width: 100%;
-      float: none;
-
-      &.scrolling-tabs {
-        float: none;
-      }
-    }
-
-    li:first-child {
-      margin-left: auto;
-    }
-
-    li:last-child {
-      margin-right: auto;
+.user-profile-image {
+  .gl-avatar {
+    @include media-breakpoint-up(md) {
+      height: 6.5rem;
+      width: 6.5rem;
     }
   }
 }
@@ -140,13 +59,10 @@
   max-width: 600px;
 }
 
-.user-calendar {
-  text-align: center;
-  min-height: 172px;
-
-  .calendar {
-    display: inline-block;
-  }
+.profile-readme-wrapper .read-more-trigger {
+  bottom: 0;
+  left: 1px;
+  right: 1px;
 }
 
 .user-calendar-activities {
@@ -158,14 +74,17 @@
 }
 
 .user-contrib-text {
-  font-size: 12px;
+  font-size: 11px;
   fill: $calendar-user-contrib-text;
 }
 
 .user-profile {
-  .profile-header {
-    .avatar-holder {
-      margin: 0 auto 10px;
+  @include media-breakpoint-up(lg) {
+    .profile-header {
+      position: sticky;
+      top: $calc-application-header-height;
+      height: $calc-application-viewport-height;
+      padding-left: $gl-spacing-scale-2;
     }
   }
 
@@ -189,23 +108,9 @@
     .gl-label-scoped {
       --label-inset-border: inset 0 0 0 1px currentColor;
     }
-
-    @include media-breakpoint-up(lg) {
-      margin-right: 5px;
-    }
-  }
-
-  .projects-block {
-    @include media-breakpoint-up(lg) {
-      margin-left: 5px;
-    }
   }
 
   @include media-breakpoint-down(xs) {
-    .cover-block {
-      padding-top: 20px;
-    }
-
     .user-profile-nav {
       a {
         margin-right: 0;
@@ -268,3 +173,45 @@
     border: 0;
   }
 }
+
+.user-profile {
+  position: relative;
+
+  @include media-breakpoint-up(lg) {
+    display: grid;
+    grid-template-columns: 1fr $right-sidebar-width;
+    gap: 2rem;
+  }
+}
+
+.user-profile-sidebar {
+  z-index: 2;
+}
+
+.user-profile-sidebar,
+.user-profile-content {
+  min-width: 1px; // grid overflow fix
+}
+
+// Home panel show profile sidebar
+// information on top
+.user-profile {
+  @include media-breakpoint-down(md) {
+    display: flex;
+    flex-direction: column;
+
+    .user-overview-page.active {
+      display: flex;
+      flex-wrap: wrap;
+
+      .user-profile-content {
+        flex-basis: 100%;
+      }
+    }
+
+    .user-profile-sidebar {
+      order: -1;
+      flex-basis: 100%;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index ebb4dffecee56bbdf76bf3e5e62d63c5a087445f..fd65485f2be82bdff091f5c22d12767eb198f391 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -325,14 +325,6 @@
       @include media-breakpoint-up(lg) {
         justify-content: flex-start;
         padding-right: $gl-spacing-scale-9;
-
-        &:not(.with-pipeline-status) {
-          .icon-wrapper:first-of-type {
-            @include media-breakpoint-up(lg) {
-              margin-left: $gl-spacing-scale-7;
-            }
-          }
-        }
       }
     }
 
@@ -569,3 +561,8 @@
     }
   }
 }
+
+.projects-list .description p {
+  @include gl-line-clamp-2;
+  margin-bottom: 0;
+}
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 83cd84c396ab48747ad9f8949a985d3a204a10d7..df8ae07721a4cdf917f4ad6379915d37a3c8583d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -143,13 +143,14 @@ def present_projects
     skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
     skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
     compact_mode = Gitlab::Utils.to_boolean(params[:compact_mode])
+    card_mode = Gitlab::Utils.to_boolean(params[:card_mode])
 
     respond_to do |format|
       format.html { render 'show' }
       format.json do
         projects = yield
 
-        pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
+        pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode, card_mode: card_mode)
       end
     end
   end
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 74c325383a1a770acc4eabbd7a9446c4cf472e38..368ffc44be9f7a71cc342030ada89b24e02e96d4 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -10,6 +10,7 @@
 - remote = false unless local_assigns[:remote] == true
 - skip_pagination = false unless local_assigns[:skip_pagination] == true
 - compact_mode = false unless local_assigns[:compact_mode] == true
+- card_mode = local_assigns[:card_mode] == true
 - css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
 - contributed_projects_current_user_empty_message_header = s_('UserProfile|Explore public groups to find projects to contribute to.')
 - contributed_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t contributed to any projects')
@@ -33,14 +34,24 @@
     - load_pipeline_status(projects) if pipeline_status
     - load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
     - load_catalog_resources(projects)
-    %ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
-      - projects.each_with_index do |project, i|
-        - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
-        = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
-          avatar: avatar, stars: stars, css_class: css_class, use_creator_avatar: use_creator_avatar,
-          forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
-          user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
-          pipeline_status: pipeline_status, compact_mode: compact_mode
+    - if card_mode
+      .projects-list.gl-text-secondary.gl-w-full.gl-display-flex.gl-flex-direction-column.gl-lg-flex-direction-row.gl-gap-4.gl-overflow-x-auto{ class: css_classes }
+        - projects.take(projects_limit).each_with_index do |project, i| # rubocop: disable CodeReuse/ActiveRecord -- it's Enumerable#take
+
+          = render "shared/projects/project_card", project: project, skip_namespace: skip_namespace,
+            avatar: avatar, stars: stars, use_creator_avatar: use_creator_avatar,
+            forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
+            user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
+            pipeline_status: pipeline_status, compact_mode: compact_mode
+    - else
+      %ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
+        - projects.take(projects_limit).each_with_index do |project, i| # rubocop: disable CodeReuse/ActiveRecord -- it's Enumerable#take
+
+          = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
+            avatar: avatar, stars: stars, use_creator_avatar: use_creator_avatar,
+            forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
+            user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
+            pipeline_status: pipeline_status, compact_mode: compact_mode
     = paginate_collection(projects, remote: remote) unless skip_pagination
   - else
     - if @contributed_projects
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 5e60e66bf844523220afdfe89e5988000ec4faf8..19f9e7b90b8d9477b5dac346b871ac158cda00b6 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,31 +1,34 @@
-- avatar = true unless local_assigns[:avatar] == false
-- stars = true unless local_assigns[:stars] == false
-- forks = true unless local_assigns[:forks] == false
-- merge_requests = true unless local_assigns[:merge_requests] == false
-- issues = true unless local_assigns[:issues] == false
-- pipeline_status = true unless local_assigns[:pipeline_status] == false
-- skip_namespace = false unless local_assigns[:skip_namespace] == true
-- access = max_project_member_access(project)
-- compact_mode = false unless local_assigns[:compact_mode] == true
+- avatar = local_assigns[:avatar].nil? || local_assigns[:avatar]
+- stars = local_assigns[:stars].nil? || local_assigns[:stars]
+- forks = local_assigns[:forks].nil? || local_assigns[:forks]
+- merge_requests = local_assigns[:merge_requests].nil? || local_assigns[:merge_requests]
+- issues = local_assigns[:issues].nil? || local_assigns[:issues]
+- pipeline_status = local_assigns[:pipeline_status].nil? || local_assigns[:pipeline_status]
+- skip_namespace = local_assigns[:skip_namespace]
+- compact_mode = local_assigns[:compact_mode]
+- use_creator_avatar = local_assigns[:use_creator_avatar]
 - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
+- access = max_project_member_access(project)
 - css_class = "gl-sm-display-flex gl-align-items-center gl-vertical-align-middle!" if project.description.blank? && !show_last_commit_as_description
 - updated_tooltip = time_ago_with_tooltip(project.last_activity_at || project.updated_at)
 - show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
 - last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon
 - css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
-- css_metadata_classes = "gl-display-flex gl-align-items-center gl-ml-5 gl-reset-color! icon-wrapper has-tooltip"
+- css_metadata_classes = "gl-display-flex gl-align-items-center gl-reset-color! icon-wrapper has-tooltip"
 
 %li.project-row
   - if avatar
     .project-cell.gl-w-11
-      = link_to project_path(project), class: dom_class(project) do
-        - if project.creator && use_creator_avatar
-          = render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '', class: 'gl-mr-5')
-        - else
-          = render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
+      .project-avatar-container.gl-mr-5.gl-relative.gl-pb-4
+        = link_to project_path(project), class: dom_class(project) do
+          - if project.creator && use_creator_avatar
+            = render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '')
+          - else
+            = render Pajamas::AvatarComponent.new(project, size: 48, alt: '')
+
   .project-cell{ class: css_class }
     .project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { testid: 'project-content', qa_project_name: project.name } }
-      .gl-display-flex.gl-align-items-center.gl-flex-wrap
+      .gl-display-flex.gl-align-items-baseline.gl-flex-wrap
         %h2.gl-font-base.gl-line-height-20.gl-my-0.gl-overflow-wrap-anywhere
           = link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
             %span.namespace-name.gl-font-weight-normal
@@ -83,32 +86,31 @@
             = _('Updated')
             = updated_tooltip
 
-  .project-cell{ class: "#{css_class} gl-display-none! gl-sm-display-table-cell!" }
-    .project-controls.gl-display-flex.gl-flex-direction-column.gl-align-items-flex-end.gl-w-full{ data: { testid: 'project_controls'} }
-      .controls.gl-display-flex.gl-align-items-center.gl-mb-2{ class: "#{css_controls_class} gl-pr-0!" }
-        - if show_pipeline_status_icon && last_pipeline.present?
-          - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
-          %span.icon-wrapper.pipeline-status
-            = render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
+  .project-cell.project-controls{ class: "#{css_class} gl-display-none! gl-sm-display-table-cell!", data: { testid: 'project_controls'} }
+    .controls.gl-display-flex.gl-align-items-center.gl-mb-2.gl-gap-4{ class: "#{css_controls_class} gl-pr-0! gl-justify-content-end!" }
+      - if show_pipeline_status_icon && last_pipeline.present?
+        - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
+        %span.icon-wrapper.pipeline-status
+          = render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
 
-        = render_if_exists 'shared/projects/archived', project: project
-        - if stars
-          = link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
-            = sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
-            = badge_count(project.star_count)
-        - if show_count?(disabled: !forks, compact_mode: compact_mode)
-          = link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
-            = sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
-            = badge_count(project.forks_count)
-        - if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
-          = link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
-            = sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
-            = badge_count(project.open_merge_requests_count)
-        - if show_count?(disabled: !issues, compact_mode: compact_mode)
-          = link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
-            = sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
-            = badge_count(project.open_issues_count)
-      .updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-end
-        %span
-          = _('Updated')
-          = updated_tooltip
+      = render_if_exists 'shared/projects/archived', project: project
+      - if stars
+        = link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
+          = sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
+          = badge_count(project.star_count)
+      - if show_count?(disabled: !forks, compact_mode: compact_mode)
+        = link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
+          = sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
+          = badge_count(project.forks_count)
+      - if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
+        = link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
+          = sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
+          = badge_count(project.open_merge_requests_count)
+      - if show_count?(disabled: !issues, compact_mode: compact_mode)
+        = link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
+          = sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
+          = badge_count(project.open_issues_count)
+    .updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-end
+      %span
+        = _('Updated')
+        = updated_tooltip
diff --git a/app/views/shared/projects/_project_card.html.haml b/app/views/shared/projects/_project_card.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b554d6ad6db828f362d638570ae0665f7b1f67a8
--- /dev/null
+++ b/app/views/shared/projects/_project_card.html.haml
@@ -0,0 +1,91 @@
+- avatar = local_assigns[:avatar].nil? || local_assigns[:avatar]
+- stars = local_assigns[:stars].nil? || local_assigns[:stars]
+- forks = local_assigns[:forks].nil? || local_assigns[:forks]
+- merge_requests = local_assigns[:merge_requests].nil? || local_assigns[:merge_requests]
+- issues = local_assigns[:issues].nil? || local_assigns[:issues]
+- pipeline_status = local_assigns[:pipeline_status].nil? || local_assigns[:pipeline_status]
+- skip_namespace = local_assigns[:skip_namespace]
+- compact_mode = local_assigns[:compact_mode]
+- use_creator_avatar = local_assigns[:use_creator_avatar]
+- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
+- updated_tooltip = time_ago_with_tooltip(project.last_activity_at || project.updated_at)
+- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
+- last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon
+- css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
+- css_metadata_classes = "gl-display-flex gl-align-items-center gl-ml-0! gl-reset-color! icon-wrapper has-tooltip"
+
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-justify-content-space-between gl-lg-w-25p gl-flex-grow-1 gl-shrink-0 gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-grow-1 gl-align-items-flex-start gl-border-b-0 gl-px-4 gl-gap-5' }, body_options: { class: 'gl-new-card-body gl-px-4 gl-py-4' }) do |c|
+  - c.with_header do
+    - if avatar
+      .project-avatar-container.gl-relative.gl-pb-4
+        = link_to project_path(project), class: dom_class(project) do
+          - if project.creator && use_creator_avatar
+            = render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '')
+          - else
+            = render Pajamas::AvatarComponent.new(project, size: 48, alt: '')
+
+    .gl-w-full.gl-pt-2.gl-word-break-word
+      .gl-display-flex.gl-align-items-center.gl-flex-wrap
+        %h2.gl-font-base.gl-line-height-20.gl-my-0
+          = link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
+            %span.namespace-name.gl-font-weight-normal
+              - if project.namespace && !skip_namespace
+                = project.namespace.human_name
+                \/
+            %span.project-name<
+              = project.name
+
+        = visibility_level_content(project)
+
+      - if show_last_commit_as_description
+        .description.gl-display-none.gl-sm-display-block.gl-mt-2.gl-font-sm
+          = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
+      - elsif project.description.present?
+        .description.gl-display-none.gl-sm-display-block.gl-mt-2.gl-font-sm
+          = markdown_field(project, :description)
+
+      - if project.topics.any?
+        .gl-mt-3.gl-ml-n1
+          = render "shared/projects/topics", project: project.present(current_user: current_user)
+      - if project.catalog_resource
+        = render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource) }
+
+      - if explore_projects_tab? && project_license_name(project)
+        %span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
+          = sprite_icon('scale', size: 14, css_class: 'gl-mr-2')
+          = project_license_name(project)
+
+      - if !explore_projects_tab?
+        = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!'
+  - c.with_body do
+    .project-controls{ data: { testid: 'project_controls'} }
+      .gl-display-flex.gl-align-items-center.gl-gap-2.gl-mb-2.gl-justify-content-space-between.gl-flex-wrap
+        .controls.gl-display-flex.gl-align-items-center.gl-gap-4{ class: "#{css_controls_class} gl-pr-0!" }
+          - if stars
+            = link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
+              = sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
+              = badge_count(project.star_count)
+          - if show_count?(disabled: !forks, compact_mode: compact_mode)
+            = link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
+              = sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
+              = badge_count(project.forks_count)
+          - if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
+            = link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
+              = sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
+              = badge_count(project.open_merge_requests_count)
+          - if show_count?(disabled: !issues, compact_mode: compact_mode)
+            = link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
+              = sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
+              = badge_count(project.open_issues_count)
+
+        .gl-display-flex.gl-align-items-center.gl-gap-2.gl-mr-n2
+          - if show_pipeline_status_icon && last_pipeline.present?
+            - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
+            %span.icon-wrapper.pipeline-status
+              = render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
+
+          = render_if_exists 'shared/projects/archived', project: project
+      .updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-start
+        %span
+          = _('Updated')
+          = updated_tooltip
diff --git a/app/views/users/_follow_user.html.haml b/app/views/users/_follow_user.html.haml
index 71f8a462cbf7bf881bb45f4a4ade286e7a9be948..953e74c1f27c1698471f8e9f7229991c3f122869 100644
--- a/app/views/users/_follow_user.html.haml
+++ b/app/views/users/_follow_user.html.haml
@@ -1,11 +1,9 @@
-- link_classes = "flex-grow-1 gl-display-inline-block"
-
 - if current_user&.following_users_allowed?(@user)
   - if current_user.following?(@user)
-    = form_tag user_unfollow_path(@user, :json), class: link_classes do
-      = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-w-full', data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do
+    = form_tag user_unfollow_path(@user, :json) do
+      = render Pajamas::ButtonComponent.new(type: :submit, button_options: { data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do
         = _('Unfollow')
   - else
-    = form_tag user_follow_path(@user, :json), class: link_classes do
-      = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
+    = form_tag user_follow_path(@user, :json) do
+      = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
         = _('Follow')
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index 3a073436aa327e12e1c093fcc9e6557972c78759..c9ccd63fae36363bffb719f762f131d7ebf2533d 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -1,46 +1,46 @@
-- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6 gl-align-self-start"
+- if can?(current_user, :read_cross_project) && @user.user_readme&.rich_viewer
+  .profile-readme-wrapper.gl-relative.gl-overflow-hidden.gl-w-full.gl-pt-5
+    .profile-readme.read-more-container.gl-relative.justify-content-center.gl-border.gl-rounded-base.gl-overflow-hidden{ data: { 'read-more-height': 400 } }
+      .read-more-content.read-more-content--has-scrim.gl-py-5.gl-px-6
+        .gl-display-flex
+          = render Pajamas::BreadcrumbComponent.new(class: 'gl-mb-4') do |c|
+            - c.with_item(text: @user.username, href: project_path(@user.user_project))
+            - c.with_item(text: @user.user_readme.path, href: @user.user_project.readme_url)
 
-.row.d-none.d-sm-flex
-  .col-12.calendar-block.gl-my-3
-    .user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } }
-      = gl_loading_icon(size: 'md', css_class: 'gl-my-8')
-      .user-calendar-error.invisible
-        = _('There was an error loading users activity calendar.')
-        %a.js-retry-load{ href: '#' }
-          = s_('UserProfile|Retry')
-- if @user.user_readme&.rich_viewer
-  .row.justify-content-center
-    .col-12.col-md-10.col-lg-8.gl-my-6
-      .gl-display-flex
-        = render Pajamas::BreadcrumbComponent.new(class: 'gl-mb-4') do |c|
-          - c.with_item(text: @user.username, href: project_path(@user.user_project))
-          - c.with_item(text: @user.user_readme.path, href: @user.user_project.readme_url)
+          - if current_user == @user
+            .gl-ml-auto
+              = link_to _('Edit file'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
+        = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
+      .js-read-more-trigger.read-more-trigger.gl-h-8.gl-absolute.gl-z-index-2.gl-bg-white.gl-px-6.gl-rounded-bottom-base
+        = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'gl-mt-4 gl-ml-n1', 'aria-label': _("Expand Readme") }) do
+          = sprite_icon('chevron-down', size: 14, css_class: 'gl-mr-1 gl-mb-n1')
+          = _("Read more")
 
-        - if current_user == @user
-          .gl-ml-auto
-            = link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
-      = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
-.row
-  .col-12.user-calendar-activities
-.row
-  %div{ class: activity_pane_class }
-    - if can?(current_user, :read_cross_project)
-      .activities-block
-        .gl-mt-5
-          .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
-            %h4.gl-flex-grow-1
-              = Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
-            = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
-          .overview-content-list.user-activity-content{ data: { href: user_activity_path, testid: 'user-activity-content' } }
-            = gl_loading_icon(size: 'md', css_class: 'loading')
+- if can?(current_user, :read_cross_project)
+  .gl-align-self-start.gl-overflow-hidden
+    .activities-block
+      .gl-display-flex.gl-align-items-baseline
+        %h2.gl-heading-3.gl-flex-grow-1{ class: 'gl-mt-5! gl-mb-3!' }
+          = Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
+        = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
 
-  - unless Feature.enabled?(:security_auto_fix) && @user.bot?
-    .col-md-12.col-lg-6
-      .projects-block
-        .gl-mt-5
-          .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
-            %h4.gl-flex-grow-1
-              = s_('UserProfile|Personal projects')
-            = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
-          .overview-content-list{ data: { href: user_projects_path } }
-            = gl_loading_icon(size: 'md', css_class: 'loading')
+      .user-calendar.gl-border.light.gl-rounded-base.gl-px-3.gl-pt-4.gl-text-center{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } }
+        = gl_loading_icon(size: 'md', css_class: 'gl-my-8')
+        .user-calendar-error.invisible
+          = _('There was an error loading users activity calendar.')
+          = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'js-retry-load' }) do
+            = s_('UserProfile|Retry')
+
+      .user-calendar-activities
+      .overview-content-list.user-activity-content.gl-mb-5{ data: { href: user_activity_path, testid: 'user-activity-content' } }
+        = gl_loading_icon(size: 'md', css_class: 'loading')
+
+- unless Feature.enabled?(:security_auto_fix) && @user.bot?
+  - if @user.personal_projects.any?
+    .projects-block.gl-w-full
+      .gl-display-flex.gl-align-items-baseline
+        %h2.gl-heading-3.gl-flex-grow-1{ class: 'gl-mt-5! gl-mb-3!' }
+          = s_('UserProfile|Personal projects')
+        = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
+      .overview-content-list{ data: { href: user_projects_path } }
+        = gl_loading_icon(size: 'md', css_class: 'loading')
diff --git a/app/views/users/_view_user_in_admin_area.html.haml b/app/views/users/_view_user_in_admin_area.html.haml
index 36b3c33d8ab3624a74653057e35a5e6604be1d3a..c8265ac9a9aeaf1ff4a167066e326b4c33f50363 100644
--- a/app/views/users/_view_user_in_admin_area.html.haml
+++ b/app/views/users/_view_user_in_admin_area.html.haml
@@ -1,4 +1,4 @@
 - if current_user && current_user.admin?
   = render Pajamas::ButtonComponent.new(href: [:admin, @user],
     icon: 'user',
-    button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })
+    button_options: { class: 'has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0f6d08dee8b9d72b3768b29c414a775c3f94929f..6bd45d165a4c47a87bb374e8a4d9cf2ef11bf906 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,105 +12,170 @@
 = content_for :meta_tags do
   = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
 
-.user-profile
-  .cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
-    %div{ class: container_class }
-      .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
+%div{ class: container_class }
+  .user-profile-header.gl-display-flex.gl-justify-content-space-between.gl-flex-direction-column.gl-md-flex-direction-row-reverse.gl-mt-5.gl-mb-2
+    %div
+      .cover-controls.gl-display-flex.gl-gap-3.gl-mb-4.gl-md-justify-content-end.gl-md-flex-direction-row-reverse
+        .js-user-profile-actions{ data: user_profile_actions_data(@user) }
         = render 'users/follow_user'
         -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
         - if @user == current_user
           = render Pajamas::ButtonComponent.new(href: user_settings_profile_path,
-            button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
+            button_options: { title: s_('UserProfile|Edit profile') }) do
             = s_("UserProfile|Edit profile")
         = render 'users/view_gpg_keys'
         = render 'users/view_user_in_admin_area'
-        .js-user-profile-actions{ data: user_profile_actions_data(@user) }
-
-      .profile-header.gl-mx-5.gl-mb-4{ class: [('gl-mb-6' if profile_tabs.empty?)] }
-        .gl-display-inline-block.gl-mx-8.gl-vertical-align-top
-          .avatar-holder
-            = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
-              = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
-          - if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user)
-            #js-user-achievements{ data: { root_url: root_url, user_id: @user.id } }
-        .gl-display-inline-block.gl-vertical-align-top.gl-text-left.gl-max-w-80
-          - if @user.blocked? || !@user.confirmed?
-            .user-info
-              %h1.cover-title.gl-my-0
-                = user_display_name(@user)
-            = render "users/profile_basic_info"
-          - else
-            .user-info
-              %h1.cover-title.gl-my-0{ itemprop: 'name' }
-                = @user.name
-                - if @user.pronouns.present?
-                  %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle
-                    = "(#{@user.pronouns})"
-                - if @user.status&.busy?
-                  = render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-vertical-align-middle')
-
-              - if @user.pronunciation.present?
-                .gl-align-items-center
-                  %p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
-
-              - if @user.status&.customized?
-                .cover-status.gl-display-inline-flex.gl-align-items-baseline.gl-mb-3
-                  = emoji_icon(@user.status.emoji, class: 'gl-mr-2')
-                  = markdown_field(@user.status, :message)
-              = render "users/profile_basic_info"
-              - user_local_time = local_time(@user.timezone)
-              - if @user.location.present? || user_local_time.present? || work_information(@user).present?
-                .gl-text-gray-900
+    .gl-display-flex.gl-flex-direction-row.gl-align-items-flex-start.gl-md-align-items-center.gl-column-gap-5.gl-mt-2.gl-sm-mt-0
+      .user-image.gl-relative.gl-py-3
+        = link_to avatar_icon_for_user(@user, 400, current_user: current_user), class: "user-profile-image", target: '_blank', rel: 'noopener noreferrer', title: s_('UserProfile|View large avatar') do
+          = render Pajamas::AvatarComponent.new(@user, alt: s_('UserProfile|User profile picture'), size: 64, avatar_options: { itemprop: "image" })
+        - if @user.status&.busy?
+          = render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-absolute gl-display-flex gl-justify-content-center gl-align-items-center gl-bottom-0 gl-left-50p gl-bg-gray-50 gl-border gl-border-white gl-translate-x-n50')
+      %div
+        %h1.gl-heading-1.gl-line-height-1.gl-mr-2{ class: 'gl-my-0!', itemprop: 'name' }
+          = user_display_name(@user)
+        .gl-font-size-h2.gl-text-gray-600.gl-font-weight-normal.gl-my-0
+          = @user.to_reference
+        - if !@user.blocked? && @user.confirmed? && @user.status&.customized?
+          .gl-my-2.cover-status.gl-font-sm.gl-pt-2.gl-display-flex.gl-flex-direction-column
+            .gl-display-inline-flex.gl-gap-3.gl-align-items-baseline
+              = emoji_icon(@user.status.emoji)
+              = markdown_field(@user.status, :message)
+  .user-profile
+    .user-profile-content
+      - if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
+        #js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
+      - unless Feature.enabled?(:profile_tabs_vue, current_user)
+        .tab-content
+          - if profile_tab?(:overview)
+            #js-overview.tab-pane.user-overview-page
+              = render "users/overview"
+
+          - if profile_tab?(:activity)
+            #activity.tab-pane
+              .flash-container
+              - if can?(current_user, :read_cross_project)
+                .content_list.user-activity-content{ data: { href: user_activity_path } }
+                .loading
+                  = gl_loading_icon(size: 'md')
+          - unless @user.bot?
+            - if profile_tab?(:groups)
+              #groups.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:contributed)
+              #contributed.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:projects)
+              #projects.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:starred)
+              #starred.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:snippets)
+              #snippets.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:followers)
+              #followers.tab-pane
+                -# This tab is always loaded via AJAX
+
+            - if profile_tab?(:following)
+              #following.tab-pane
+                -# This tab is always loaded via AJAX
+
+        .loading.hide
+          .gl-spinner.gl-spinner-md
+
+      - if profile_tabs.empty?
+        .svg-content
+          = image_tag 'illustrations/profile_private_mode.svg'
+        .text-content.text-center
+          %h4
+            - if @user.blocked?
+              = s_('UserProfile|This user is blocked')
+            - else
+              = s_('UserProfile|This user has a private profile')
+    .user-profile-sidebar
+      .profile-header.gl-pb-5.gl-pt-3.gl-overflow-y-auto.gl-sm-pr-4
+        .gl-vertical-align-top.gl-text-left.gl-max-w-80.gl-overflow-wrap-anywhere
+          .user-info
+            - if !@user.blocked? && @user.confirmed?
+              .gl-display-flex.gl-gap-4.gl-flex-direction-column
+                - if @user.pronouns.present? || @user.pronunciation.present? || @user.bio.present?
+                  %div
+                    %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|About')
+                    .gl-display-flex.gl-gap-2.gl-flex-direction-column
+                      - if @user.pronouns.present? || @user.pronunciation.present?
+                        %div
+                          - if @user.pronunciation.present?
+                            %div= sprintf(s_("UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"), { pronunciation: @user.pronunciation, div_start: '<div class="gl-font-sm gl-text-secondary gl-display-inline-flex">', div_end: '</div>' }).html_safe
+                          - if @user.pronouns.present?
+                            %div= sprintf(s_("UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"), { pronouns: @user.pronouns, div_start: '<div class="gl-font-sm gl-text-secondary gl-display-inline-flex">', div_end: '</div>' }).html_safe
+                      - if @user.bio.present?
+                        %p.profile-user-bio.gl-mb-0
+                          = @user.bio
+
+                - if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user)
+                  #js-user-achievements{ data: { root_url: root_url, user_id: @user.id } }
+
+                - user_local_time = local_time(@user.timezone)
+                %div{ itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' }
+                  %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Info')
+                  - if work_information(@user).present?
+                    .gl-mb-2
+                      = sprite_icon('work', css_class: 'fgray')
+                      %span.gl-ml-1
+                        = work_information(@user, with_schema_markup: true)
                   - if @user.location.present?
-                    = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do
+                    .gl-mb-2
                       = sprite_icon('location', css_class: 'fgray')
-                      %span{ itemprop: 'addressLocality' }
+                      %span.gl-ml-1{ itemprop: 'addressLocality' }
                         = @user.location
                   - if user_local_time.present?
-                    = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
+                    .gl-mb-2{ data: { testid: 'user-local-time' } }
                       = sprite_icon('clock', css_class: 'fgray')
-                      %span
+                      %span.gl-ml-1
                         = user_local_time
-                  - if work_information(@user).present?
-                    = render 'middle_dot_divider', stacking: true do
-                      = sprite_icon('work', css_class: 'fgray')
-                      %span
-                        = work_information(@user, with_schema_markup: true)
-              .gl-text-gray-900
-                - if @user.skype.present?
-                  = render 'middle_dot_divider' do
-                    = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do
-                      = sprite_icon('skype', css_class: 'skype-icon')
-                - if @user.linkedin.present?
-                  = render 'middle_dot_divider' do
-                    = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = sprite_icon('linkedin', css_class: 'linkedin-icon')
-                - if @user.twitter.present?
-                  = render 'middle_dot_divider', breakpoint: 'sm' do
-                    = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = sprite_icon('x', css_class: 'x-icon')
-                - if @user.discord.present?
-                  = render 'middle_dot_divider', breakpoint: 'sm' do
-                    = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = sprite_icon('discord', css_class: 'discord-icon')
-                - if @user.mastodon.present?
-                  = render 'middle_dot_divider', breakpoint: 'sm' do
-                    = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = sprite_icon('mastodon', css_class: 'mastodon-icon')
-                - if @user.website_url.present?
-                  = render 'middle_dot_divider', stacking: true do
-                    - if Feature.enabled?(:security_auto_fix) && @user.bot?
-                      = sprite_icon('question-o', css_class: 'gl-text-blue-500')
-                    = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
-                - if display_public_email?(@user)
-                  = render 'middle_dot_divider', stacking: true do
-                    = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email'
-
-              -# Ensure this stays indented one level less than the social links
-              -# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118314
-              - if @user.bio.present? && @user.confirmed? && !@user.blocked?
-                %p.profile-user-bio.gl-mb-3
-                  = @user.bio
+                  = sprite_icon('calendar', css_class: 'fgray')
+                  %span.gl-ml-1= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
+
+                - if @user.website_url.present? || display_public_email?(@user) || @user.skype.present? || @user.linkedin.present? || @user.twitter.present? || @user.mastodon.present? || @user.discord.present?
+                  .gl-text-gray-900
+                    %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Contact')
+                    - if @user.website_url.present?
+                      .gl-mb-2
+                        - if Feature.enabled?(:security_auto_fix) && @user.bot?
+                          = sprite_icon('question-o', css_class: 'gl-text-blue-500')
+                        = sprite_icon('earth', css_class: 'fgray')
+                        = link_to @user.short_website_url, @user.full_website_url, class: 'gl-text-gray-900 gl-ml-1', target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
+                    - if display_public_email?(@user)
+                      .gl-mb-2
+                        = sprite_icon('mail', css_class: 'fgray')
+                        = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'gl-text-gray-900 gl-ml-1', itemprop: 'email'
+                    - if @user.skype.present?
+                      .gl-mb-2
+                        = sprite_icon('skype', css_class: 'fgray')
+                        = link_to @user.skype, "skype:#{@user.skype}", class: 'gl-text-gray-900 gl-ml-1', title: "Skype"
+                    - if @user.linkedin.present?
+                      .gl-mb-2
+                        = sprite_icon('linkedin', css_class: 'fgray')
+                        = link_to @user.linkedin, linkedin_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow'
+                    - if @user.twitter.present?
+                      .gl-mb-2
+                        = sprite_icon('x', css_class: 'fgray')
+                        = link_to @user.twitter, twitter_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow'
+                    - if @user.mastodon.present?
+                      .gl-mb-2
+                        = sprite_icon('mastodon', css_class: 'fgray')
+                        = link_to @user.mastodon, mastodon_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow'
+                    - if @user.discord.present?
+                      .gl-mb-2
+                        = sprite_icon('discord', css_class: 'fgray')
+                        = link_to @user.discord, discord_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow'
 
       -# TODO: Remove this with the removal of the old navigation.
       -# See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
@@ -144,7 +209,7 @@
                     = s_('UserProfile|Personal projects')
               - if profile_tab?(:starred)
                 %li.js-starred-tab
-                  = link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
+                  = link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json), card_mode: true } do
                     = s_('UserProfile|Starred projects')
               - if profile_tab?(:snippets)
                 %li.js-snippets-tab
@@ -157,67 +222,7 @@
                     = gl_badge_tag @user.followers.count, size: :sm
               - if profile_tab?(:following)
                 %li.js-following-tab
-                  = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
+                  = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), testid: 'following_tab' } do
                     = s_('UserProfile|Following')
                     = gl_badge_tag @user.followees.count, size: :sm
-    - if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
-      #js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
-  %div{ class: container_class }
-    - unless Feature.enabled?(:profile_tabs_vue, current_user)
-      .tab-content
-        - if profile_tab?(:overview)
-          #js-overview.tab-pane
-            = render "users/overview"
-
-        - if profile_tab?(:activity)
-          #activity.tab-pane
-            .row
-              .col-12
-                .flash-container
-                - if can?(current_user, :read_cross_project)
-                  %h4.prepend-top-20
-                    = s_('UserProfile|Most Recent Activity')
-                  .content_list.user-activity-content{ data: { href: user_activity_path } }
-                  .loading
-                    = gl_loading_icon(size: 'md')
-        - unless @user.bot?
-          - if profile_tab?(:groups)
-            #groups.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:contributed)
-            #contributed.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:projects)
-            #projects.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:starred)
-            #starred.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:snippets)
-            #snippets.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:followers)
-            #followers.tab-pane
-              -# This tab is always loaded via AJAX
-
-          - if profile_tab?(:following)
-            #following.tab-pane
-              -# This tab is always loaded via AJAX
-
-      .loading.hide
-        .gl-spinner.gl-spinner-md
-
-    - if profile_tabs.empty?
-      .svg-content
-        = image_tag 'illustrations/profile_private_mode.svg'
-      .text-content.text-center
-        %h4
-          - if @user.blocked?
-            = s_('UserProfile|This user is blocked')
-          - else
-            = s_('UserProfile|This user has a private profile')
+
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 108921089fe4ef32cc5354c452ea45142012efd6..c97bfe8e8d0384d9bd98f970a1cb1f7a31f0d98d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2722,6 +2722,9 @@ msgstr ""
 msgid "Achievements|%{namespace_link} awarded you the %{bold_start}%{achievement_name}%{bold_end} achievement!"
 msgstr ""
 
+msgid "Achievements|Achievements"
+msgstr ""
+
 msgid "Achievements|Awarded %{timeAgo} by %{namespace}"
 msgstr ""
 
@@ -18594,6 +18597,9 @@ msgstr ""
 msgid "Edit environment"
 msgstr ""
 
+msgid "Edit file"
+msgstr ""
+
 msgid "Edit files in the editor and commit changes here"
 msgstr ""
 
@@ -20329,6 +20335,9 @@ msgstr ""
 msgid "Expand AI-generated summary"
 msgstr ""
 
+msgid "Expand Readme"
+msgstr ""
+
 msgid "Expand all"
 msgstr ""
 
@@ -53953,6 +53962,9 @@ msgstr ""
 msgid "UserProfile|%{id} · created %{created} by %{author}"
 msgstr ""
 
+msgid "UserProfile|About"
+msgstr ""
+
 msgid "UserProfile|Activity"
 msgstr ""
 
@@ -53977,6 +53989,9 @@ msgstr ""
 msgid "UserProfile|Busy"
 msgstr ""
 
+msgid "UserProfile|Contact"
+msgstr ""
+
 msgid "UserProfile|Contributed projects"
 msgstr ""
 
@@ -54007,10 +54022,10 @@ msgstr ""
 msgid "UserProfile|Groups are the best way to manage projects and members."
 msgstr ""
 
-msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
+msgid "UserProfile|Info"
 msgstr ""
 
-msgid "UserProfile|Most Recent Activity"
+msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
 msgstr ""
 
 msgid "UserProfile|No snippets found."
@@ -54022,7 +54037,10 @@ msgstr ""
 msgid "UserProfile|Personal projects"
 msgstr ""
 
-msgid "UserProfile|Pronounced as: %{pronunciation}"
+msgid "UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"
+msgstr ""
+
+msgid "UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"
 msgstr ""
 
 msgid "UserProfile|Retry"
@@ -54085,9 +54103,15 @@ msgstr ""
 msgid "UserProfile|User profile navigation"
 msgstr ""
 
+msgid "UserProfile|User profile picture"
+msgstr ""
+
 msgid "UserProfile|View all"
 msgstr ""
 
+msgid "UserProfile|View large avatar"
+msgstr ""
+
 msgid "UserProfile|View user in admin area"
 msgstr ""
 
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 291c40f0f6b382888302814fdec6767e5615411e..c750cd0a8448190686ebce3efb94bab62587aabc 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -224,24 +224,6 @@ def selected_day_activities(visible: true)
       end
     end
 
-    describe 'on smaller screens' do
-      shared_examples 'hidden activity calendar' do
-        include_context 'when user page is visited'
-
-        it 'hides the activity calender' do
-          expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
-        end
-      end
-
-      context 'when screen size is xs' do
-        before do
-          resize_screen_xs
-        end
-
-        it_behaves_like 'hidden activity calendar'
-      end
-    end
-
     describe 'first_day_of_week setting' do
       context 'when first day of the week is set to Monday' do
         before do
@@ -356,24 +338,6 @@ def selected_day_activities(visible: true)
       end
     end
 
-    describe 'on smaller screens' do
-      shared_examples 'hidden activity calendar' do
-        include_context 'when user page is visited'
-
-        it 'hides the activity calender' do
-          expect(page).not_to have_css('[data-testid="contrib-calendar"]')
-        end
-      end
-
-      context 'when screen size is xs' do
-        before do
-          resize_screen_xs
-        end
-
-        it_behaves_like 'hidden activity calendar'
-      end
-    end
-
     describe 'first_day_of_week setting' do
       context 'when first day of the week is set to Monday' do
         before do
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 7e4308106be6359a401a284c6690b533d4336e9f..08d9ec5f3a9f364b681f8d407323c3f064ff5142 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -51,14 +51,14 @@
       update_username(new_username)
       visit new_user_path
       expect(page).to have_current_path(new_user_path, ignore_query: true)
-      expect(find('.user-info')).to have_content(new_username)
+      expect(find('.user-profile-header')).to have_content(new_username)
     end
 
     it 'the old user path redirects to the new path' do
       update_username(new_username)
       visit old_user_path
       expect(page).to have_current_path(new_user_path, ignore_query: true)
-      expect(find('.user-info')).to have_content(new_username)
+      expect(find('.user-profile-header')).to have_content(new_username)
     end
 
     context 'with a project' do
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 7edfb542594ffe8369d8bc3a49db7c62aa4c14bb..ac5682991b31cb5d823e12b921c388a7110fd7cf 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -48,7 +48,7 @@
       it 'shows expected content', :js do
         visit(user_path(user))
 
-        page.within ".cover-block" do
+        page.within ".user-profile-header" do
           expect(page).to have_content user.name
           expect(page).to have_content user.username
         end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 88d2d1c46bc174db7f2a7b253f79a0f485c67ead..58fb8faf05e09aa4265095cd8223909400f01990 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -51,7 +51,7 @@
 
       visit user_path(user)
 
-      expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
+      expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=64"]))
 
       # Cheating here to verify something that isn't user-facing, but is important
       expect(user.reload.avatar.file).to exist
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 1da61ecb86861d4296d74308fe57ca18092ecbe8..727b87c69dd8cea5ec35efe2093b0f82ea49053c 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -86,22 +86,6 @@ def push_code_contribution
   end
 
   describe 'projects section' do
-    describe 'user has no personal projects' do
-      include_context 'visit overview tab'
-
-      it 'shows an empty project list with an info message' do
-        page.within('.projects-block') do
-          expect(page).to have_selector('.loading', visible: false)
-          expect(page).to have_content('You haven\'t created any personal projects.')
-          expect(page).not_to have_selector('.project-row')
-        end
-      end
-
-      it 'does not show a link to the project list' do
-        expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: false)
-      end
-    end
-
     describe 'user has a personal project' do
       before do
         create(:project, :private, namespace: user.namespace, creator: user) { |p| p.add_maintainer(user) }
@@ -111,7 +95,7 @@ def push_code_contribution
 
       it 'shows one entry in the list of projects' do
         page.within('.projects-block') do
-          expect(page).to have_selector('.project-row', count: 1)
+          expect(page).to have_selector('.gl-card', count: 1)
         end
       end
 
@@ -119,9 +103,9 @@ def push_code_contribution
         expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: true)
       end
 
-      it 'shows projects in "compact mode"' do
+      it 'shows projects in "card mode"' do
         page.within('#js-overview .projects-block') do
-          expect(find('.js-projects-list-holder')).to have_selector('.compact')
+          expect(find('.js-projects-list-holder')).to have_css('.gl-card')
         end
       end
     end
@@ -135,9 +119,9 @@ def push_code_contribution
 
       include_context 'visit overview tab'
 
-      it 'shows max. ten entries in the list of projects' do
+      it 'shows max. 3 entries in the list of projects' do
         page.within('.projects-block') do
-          expect(page).to have_selector('.project-row', count: 10)
+          expect(page).to have_selector('.gl-card', count: 3)
         end
       end
 
@@ -315,7 +299,7 @@ def push_code_contribution
       end
 
       it 'shows projects panel' do
-        expect(page).to have_selector('.projects-block')
+        expect(page).not_to have_selector('.projects-block')
       end
     end
   end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 730c31df899f8ed454d604000f4c790d03787b4d..f0ca0715e145d6896b824d04b3d138f31d8dee54 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -13,7 +13,7 @@
     end
 
     it 'shows the RSS link with overflow menu', :js do
-      page.within('.user-cover-block') do
+      page.within('.user-profile-header') do
         find_by_testid('base-dropdown-toggle').click
       end
 
@@ -27,7 +27,7 @@
     end
 
     it 'has an RSS without a feed token', :js do
-      page.within('.user-cover-block') do
+      page.within('.user-profile-header') do
         find_by_testid('base-dropdown-toggle').click
       end
 
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index c56b261fe282c2be3c04e863974c30e02b6bb9ca..754de6ed67f280859bcc5ec96a4f7ce441ca55d1 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -12,7 +12,7 @@
   it 'shows copy user id action in the dropdown', :js do
     subject
 
-    page.within('.user-cover-block') do
+    page.within('.cover-controls') do
       find_by_testid('base-dropdown-toggle').click
     end
 
@@ -305,7 +305,7 @@
     end
 
     it 'shows user name as blocked' do
-      expect(page).to have_css(".cover-title", text: 'Blocked user')
+      expect(page).to have_css(".user-profile-header", text: 'Blocked user')
     end
 
     it 'shows no additional fields' do
@@ -343,7 +343,7 @@
       end
 
       it 'shows user name as unconfirmed' do
-        expect(page).to have_css(".cover-title", text: 'Unconfirmed user')
+        expect(page).to have_css(".user-profile-header", text: 'Unconfirmed user')
       end
 
       it 'shows no tab' do
@@ -393,7 +393,7 @@
 
     subject
 
-    expect(page).to have_content("(they/them)")
+    expect(page).to have_content("Pronouns: they/them")
   end
 
   it 'shows the pronunctiation of the user if there was one' do
@@ -435,12 +435,6 @@
       stub_feature_flags(profile_tabs_vue: false)
     end
 
-    it 'shows the most recent activity' do
-      subject
-
-      expect(page).to have_content('Most Recent Activity')
-    end
-
     context 'when external authorization is enabled' do
       before do
         enable_external_authorization_service_check
diff --git a/spec/frontend/profile/components/activity_calendar_spec.js b/spec/frontend/profile/components/activity_calendar_spec.js
index fb9dc7b22f73a49057427b600bb1a6634d1fb2f5..043e717538c2d5859c8618e79f62535f1945af63 100644
--- a/spec/frontend/profile/components/activity_calendar_spec.js
+++ b/spec/frontend/profile/components/activity_calendar_spec.js
@@ -1,5 +1,4 @@
 import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import * as GitLabUIUtils from '@gitlab/ui/dist/utils';
 
 import ActivityCalendar from '~/profile/components/activity_calendar.vue';
 import AjaxCache from '~/lib/utils/ajax_cache';
@@ -57,23 +56,6 @@ describe('ActivityCalendar', () => {
       expect(findCalendar().exists()).toBe(true);
       expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true);
     });
-
-    describe('when window is resized', () => {
-      it('re-renders the calendar', async () => {
-        createComponent();
-
-        await waitForPromises();
-
-        mockSuccessfulApiRequest();
-        window.innerWidth = 1200;
-        window.dispatchEvent(new Event('resize'));
-
-        await waitForPromises();
-
-        expect(findCalendar().exists()).toBe(true);
-        expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2);
-      });
-    });
   });
 
   describe('when API request is not successful', () => {
@@ -105,16 +87,4 @@ describe('ActivityCalendar', () => {
       });
     });
   });
-
-  describe('when screen is extra small', () => {
-    beforeEach(() => {
-      GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs');
-    });
-
-    it('does not render the calendar', () => {
-      createComponent();
-
-      expect(findCalendar().exists()).toBe(false);
-    });
-  });
 });
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 6b1acfef8f553de67bc7129a28fda9d1ebdd4ced..a4104c39e04b135a0fae10a18269d5e83b05260f 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -85,3 +85,41 @@ describe('Read more click-to-expand functionality', () => {
     });
   });
 });
+
+describe('data-read-more-height defines when to show the read-more button', () => {
+  const findTrigger = () => document.querySelectorAll('.js-read-more-trigger');
+
+  afterEach(() => {
+    resetHTMLFixture();
+  });
+
+  it('if not set shows button all the time', () => {
+    setHTMLFixture(`
+      <div class="read-more-container">
+        <p class="read-more-content">Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.</p>
+        <button type="button" class="js-read-more-trigger">
+          Button text
+        </button>
+      </div>
+    `);
+
+    initReadMore();
+
+    expect(findTrigger().length).toBe(1);
+  });
+
+  it('if set hides button as threshold is met', () => {
+    setHTMLFixture(`
+      <div class="read-more-container" data-read-more-height="120">
+        <p class="read-more-content">Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.</p>
+        <button type="button" class="js-read-more-trigger">
+          Button text
+      </button>
+      </div>
+    `);
+
+    initReadMore();
+
+    expect(findTrigger().length).toBe(0);
+  });
+});
diff --git a/spec/views/shared/projects/_list.html.haml_spec.rb b/spec/views/shared/projects/_list.html.haml_spec.rb
index 1b6c4e00c97f51f348439a24ef403ed834ecd72e..ff295662e3c6e22731ae8f297aa08351901ef252 100644
--- a/spec/views/shared/projects/_list.html.haml_spec.rb
+++ b/spec/views/shared/projects/_list.html.haml_spec.rb
@@ -31,6 +31,20 @@
       expect(rendered).not_to have_css('a.issues')
       expect(rendered).not_to have_css('a.merge-requests')
     end
+
+    it 'renders list in list view' do
+      expect(rendered).not_to have_css('.gl-new-card')
+    end
+  end
+
+  context 'with projects in card mode' do
+    let(:projects) { build_stubbed_list(:project, 1) }
+
+    it 'renders card mode when set to true' do
+      render template: 'shared/projects/_list', locals: { card_mode: true }
+
+      expect(rendered).to have_css('.gl-new-card')
+    end
   end
 
   context 'without projects' do
diff --git a/spec/views/shared/projects/_project_card.html.haml_spec.rb b/spec/views/shared/projects/_project_card.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f4e31176af0a522de4490fbc5c3ea0455d13d06
--- /dev/null
+++ b/spec/views/shared/projects/_project_card.html.haml_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'shared/projects/_project_card.html.haml', feature_category: :shared do
+  let(:project) { build(:project) }
+
+  before do
+    allow(view)
+      .to receive(:current_application_settings)
+      .and_return(Gitlab::CurrentSettings.current_application_settings)
+    allow(view).to receive(:can?).and_return(true)
+  end
+
+  it 'renders as a card component' do
+    render 'shared/projects/project_card', use_creator_avatar: true, project: project
+
+    expect(rendered).to have_selector('.gl-new-card')
+  end
+
+  it 'renders creator avatar if project has a creator' do
+    render 'shared/projects/project_card', use_creator_avatar: true, project: project
+
+    expect(rendered).to have_selector('img.gl-avatar')
+  end
+
+  it 'renders a generic avatar if project does not have a creator' do
+    project.creator = nil
+
+    render 'shared/projects/project_card', use_creator_avatar: true, project: project
+
+    expect(rendered).to have_selector('.gl-avatar-identicon')
+  end
+end