about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/icons/floppyDisk_stroke2_corner0_rounded.svg1
-rw-r--r--bskyweb/templates/base.html9
-rw-r--r--package.json1
-rw-r--r--src/alf/atoms.ts22
-rw-r--r--src/alf/index.tsx1
-rw-r--r--src/alf/util/useGutterStyles.ts21
-rw-r--r--src/components/Dialog/index.web.tsx2
-rw-r--r--src/components/Layout.tsx100
-rw-r--r--src/components/Layout/Header/index.tsx199
-rw-r--r--src/components/Layout/README.md172
-rw-r--r--src/components/Layout/const.ts16
-rw-r--r--src/components/Layout/context.ts5
-rw-r--r--src/components/Layout/index.tsx188
-rw-r--r--src/components/LikedByList.tsx13
-rw-r--r--src/components/Lists.tsx34
-rw-r--r--src/components/dms/MessagesListHeader.tsx36
-rw-r--r--src/components/forms/DateField/index.android.tsx1
-rw-r--r--src/components/icons/FloppyDisk.tsx5
-rw-r--r--src/lib/hooks/useWebBodyScrollLock.ts31
-rw-r--r--src/screens/Deactivated.tsx21
-rw-r--r--src/screens/Hashtag.tsx71
-rw-r--r--src/screens/Messages/ChatList.tsx160
-rw-r--r--src/screens/Messages/Conversation.tsx9
-rw-r--r--src/screens/Messages/Settings.tsx16
-rw-r--r--src/screens/Moderation/index.tsx47
-rw-r--r--src/screens/Onboarding/Layout.tsx6
-rw-r--r--src/screens/Post/PostLikedBy.tsx10
-rw-r--r--src/screens/Post/PostQuotes.tsx2
-rw-r--r--src/screens/Post/PostRepostedBy.tsx2
-rw-r--r--src/screens/Profile/KnownFollowers.tsx29
-rw-r--r--src/screens/Profile/Sections/Labels.tsx7
-rw-r--r--src/screens/Settings/AboutSettings.tsx10
-rw-r--r--src/screens/Settings/AccessibilitySettings.tsx10
-rw-r--r--src/screens/Settings/AccountSettings.tsx10
-rw-r--r--src/screens/Settings/AppIconSettings.tsx12
-rw-r--r--src/screens/Settings/AppPasswords.tsx10
-rw-r--r--src/screens/Settings/AppearanceSettings.tsx10
-rw-r--r--src/screens/Settings/ContentAndMediaSettings.tsx10
-rw-r--r--src/screens/Settings/ExternalMediaPreferences.tsx14
-rw-r--r--src/screens/Settings/FollowingFeedPreferences.tsx10
-rw-r--r--src/screens/Settings/LanguageSettings.tsx10
-rw-r--r--src/screens/Settings/NotificationSettings.tsx10
-rw-r--r--src/screens/Settings/PrivacyAndSecuritySettings.tsx10
-rw-r--r--src/screens/Settings/Settings.tsx10
-rw-r--r--src/screens/Settings/ThreadPreferences.tsx10
-rw-r--r--src/screens/SignupQueued.tsx3
-rw-r--r--src/screens/StarterPack/Wizard/index.tsx81
-rw-r--r--src/view/com/feeds/FeedPage.tsx2
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx126
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx124
-rw-r--r--src/view/com/lightbox/Lightbox.web.tsx16
-rw-r--r--src/view/com/lists/MyLists.tsx7
-rw-r--r--src/view/com/modals/Modal.web.tsx4
-rw-r--r--src/view/com/notifications/Feed.tsx23
-rw-r--r--src/view/com/pager/PagerWithHeader.web.tsx60
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx5
-rw-r--r--src/view/com/post-thread/PostQuotes.tsx3
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx16
-rw-r--r--src/view/com/post-thread/PostThread.tsx5
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx3
-rw-r--r--src/view/com/profile/ProfileFollows.tsx3
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx103
-rw-r--r--src/view/com/util/List.web.tsx115
-rw-r--r--src/view/com/util/LoadingScreen.tsx9
-rw-r--r--src/view/com/util/SimpleViewHeader.tsx114
-rw-r--r--src/view/com/util/ViewHeader.tsx270
-rw-r--r--src/view/com/util/Views.tsx7
-rw-r--r--src/view/com/util/Views.web.tsx31
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx4
-rw-r--r--src/view/screens/Feeds.tsx128
-rw-r--r--src/view/screens/Lists.tsx72
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx26
-rw-r--r--src/view/screens/ModerationModlists.tsx64
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx28
-rw-r--r--src/view/screens/Notifications.tsx178
-rw-r--r--src/view/screens/PostThread.tsx6
-rw-r--r--src/view/screens/Profile.tsx6
-rw-r--r--src/view/screens/ProfileFeed.tsx9
-rw-r--r--src/view/screens/ProfileFollowers.tsx2
-rw-r--r--src/view/screens/ProfileFollows.tsx2
-rw-r--r--src/view/screens/ProfileList.tsx10
-rw-r--r--src/view/screens/SavedFeeds.tsx238
-rw-r--r--src/view/screens/Search/Search.tsx236
-rw-r--r--src/view/shell/Composer.web.tsx11
-rw-r--r--src/view/shell/desktop/LeftNav.tsx131
-rw-r--r--src/view/shell/desktop/RightNav.tsx11
-rw-r--r--src/view/shell/index.web.tsx54
-rw-r--r--web/index.html9
-rw-r--r--yarn.lock31
89 files changed, 1721 insertions, 2048 deletions
diff --git a/assets/icons/floppyDisk_stroke2_corner0_rounded.svg b/assets/icons/floppyDisk_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..b9a42f759
--- /dev/null
+++ b/assets/icons/floppyDisk_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z" clip-rule="evenodd"/></svg>
diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
index 4c02805e3..8eb78fffd 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -40,7 +40,6 @@
     }
     html {
       background-color: white;
-      scrollbar-gutter: stable both-edges;
     }
     @media (prefers-color-scheme: dark) {
       html {
@@ -76,9 +75,15 @@
       top: 50%;
       transform: translateX(-50%) translateY(-50%) translateY(-50px);
     }
-    /* We need this style to prevent web dropdowns from shifting the display when opening */
+    /**
+     * We need these styles to prevent shifting due to scrollbar show/hide on
+     * OSs that have them enabled by default. This also handles cases where the
+     * screen wouldn't otherwise scroll, and therefore hide the scrollbar and
+     * shift the content, by forcing the page to show a scrollbar.
+     */
     body {
       width: 100%;
+      overflow-y: scroll;
     }
   </style>
 
diff --git a/package.json b/package.json
index 6372dd5f5..6c35d169e 100644
--- a/package.json
+++ b/package.json
@@ -193,6 +193,7 @@
     "react-native-web": "~0.19.11",
     "react-native-web-webview": "^1.0.2",
     "react-native-webview": "13.10.2",
+    "react-remove-scroll-bar": "^2.3.6",
     "react-responsive": "^9.0.2",
     "react-textarea-autosize": "^8.5.3",
     "rn-fetch-blob": "^0.12.0",
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 1f08eb7e1..0870c5767 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -1,7 +1,8 @@
 import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
 
 import * as tokens from '#/alf/tokens'
-import {ios, native, web} from '#/alf/util/platform'
+import {ios, native, platform, web} from '#/alf/util/platform'
+import * as Layout from '#/components/Layout'
 
 export const atoms = {
   debug: {
@@ -21,6 +22,9 @@ export const atoms = {
   relative: {
     position: 'relative',
   },
+  sticky: web({
+    position: 'sticky',
+  }),
   inset_0: {
     top: 0,
     left: 0,
@@ -941,4 +945,20 @@ export const atoms = {
     transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
     transitionDuration: '100ms',
   }),
+
+  /**
+   * {@link Layout.SCROLLBAR_OFFSET}
+   */
+  scrollbar_offset: platform({
+    web: {
+      transform: [
+        {
+          translateX: Layout.SCROLLBAR_OFFSET,
+        },
+      ],
+    },
+    native: {
+      transform: [],
+    },
+  }) as {transform: Exclude<ViewStyle['transform'], string | undefined>},
 } as const
diff --git a/src/alf/index.tsx b/src/alf/index.tsx
index 5d08722ff..a96803c56 100644
--- a/src/alf/index.tsx
+++ b/src/alf/index.tsx
@@ -20,6 +20,7 @@ export * from '#/alf/types'
 export * from '#/alf/util/flatten'
 export * from '#/alf/util/platform'
 export * from '#/alf/util/themeSelector'
+export * from '#/alf/util/useGutterStyles'
 
 export type Alf = {
   themeName: ThemeName
diff --git a/src/alf/util/useGutterStyles.ts b/src/alf/util/useGutterStyles.ts
new file mode 100644
index 000000000..64b246fdd
--- /dev/null
+++ b/src/alf/util/useGutterStyles.ts
@@ -0,0 +1,21 @@
+import React from 'react'
+
+import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
+
+export function useGutterStyles({
+  top,
+  bottom,
+}: {
+  top?: boolean
+  bottom?: boolean
+} = {}) {
+  const {gtMobile} = useBreakpoints()
+  return React.useMemo<ViewStyleProp['style']>(() => {
+    return [
+      a.px_lg,
+      top && a.pt_md,
+      bottom && a.pb_md,
+      gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
+    ]
+  }, [gtMobile, top, bottom])
+}
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 6b92eee3e..e45133dc5 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react'
 import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
 import {useFocusGuards} from '@radix-ui/react-focus-guards'
 import {FocusScope} from '@radix-ui/react-focus-scope'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {logger} from '#/logger'
 import {useDialogStateControlContext} from '#/state/dialogs'
@@ -103,6 +104,7 @@ export function Outer({
       {isOpen && (
         <Portal>
           <Context.Provider value={context}>
+            <RemoveScrollBar />
             <TouchableWithoutFeedback
               accessibilityHint={undefined}
               accessibilityLabel={_(msg`Close active dialog`)}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
deleted file mode 100644
index ea11e2217..000000000
--- a/src/components/Layout.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, {useContext, useMemo} from 'react'
-import {View, ViewStyle} from 'react-native'
-import {StyleProp} from 'react-native'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
-import {CenteredView} from '#/view/com/util/Views'
-import {atoms as a} from '#/alf'
-
-// Every screen should have a Layout component wrapping it.
-// This component provides a default padding for the top of the screen.
-// This allows certain screens to avoid the top padding if they want to.
-
-const LayoutContext = React.createContext({
-  withinScreen: false,
-  topPaddingDisabled: false,
-  withinScrollView: false,
-})
-
-/**
- * Every screen should have a Layout.Screen component wrapping it.
- * This component provides a default padding for the top of the screen
- * and height/minHeight
- */
-let Screen = ({
-  disableTopPadding = false,
-  style,
-  ...props
-}: React.ComponentProps<typeof View> & {
-  disableTopPadding?: boolean
-  style?: StyleProp<ViewStyle>
-}): React.ReactNode => {
-  const {top} = useSafeAreaInsets()
-  const context = useMemo(
-    () => ({
-      withinScreen: true,
-      topPaddingDisabled: disableTopPadding,
-      withinScrollView: false,
-    }),
-    [disableTopPadding],
-  )
-  return (
-    <LayoutContext.Provider value={context}>
-      <View
-        style={[
-          {paddingTop: disableTopPadding ? 0 : top},
-          a.util_screen_outer,
-          style,
-        ]}
-        {...props}
-      />
-    </LayoutContext.Provider>
-  )
-}
-Screen = React.memo(Screen)
-export {Screen}
-
-let Header = (
-  props: React.ComponentProps<typeof ViewHeader>,
-): React.ReactNode => {
-  const {withinScrollView} = useContext(LayoutContext)
-  if (!withinScrollView) {
-    return (
-      <CenteredView topBorder={false} sideBorders>
-        <ViewHeader showOnDesktop showBorder {...props} />
-      </CenteredView>
-    )
-  } else {
-    return <ViewHeader showOnDesktop showBorder {...props} />
-  }
-}
-Header = React.memo(Header)
-export {Header}
-
-let Content = ({
-  style,
-  contentContainerStyle,
-  ...props
-}: React.ComponentProps<typeof ScrollView> & {
-  style?: StyleProp<ViewStyle>
-  contentContainerStyle?: StyleProp<ViewStyle>
-}): React.ReactNode => {
-  const context = useContext(LayoutContext)
-  const newContext = useMemo(
-    () => ({...context, withinScrollView: true}),
-    [context],
-  )
-  return (
-    <LayoutContext.Provider value={newContext}>
-      <ScrollView
-        style={[a.flex_1, style]}
-        contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]}
-        {...props}
-      />
-    </LayoutContext.Provider>
-  )
-}
-Content = React.memo(Content)
-export {Content}
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
new file mode 100644
index 000000000..a35a09537
--- /dev/null
+++ b/src/components/Layout/Header/index.tsx
@@ -0,0 +1,199 @@
+import {createContext, useCallback, useContext} from 'react'
+import {GestureResponderEvent, View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {HITSLOP_30} from '#/lib/constants'
+import {NavigationProp} from '#/lib/routes/types'
+import {isIOS} from '#/platform/detection'
+import {useSetDrawerOpen} from '#/state/shell'
+import {
+  atoms as a,
+  platform,
+  TextStyleProp,
+  useBreakpoints,
+  useGutterStyles,
+  useTheme,
+} from '#/alf'
+import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
+import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
+import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {
+  BUTTON_VISUAL_ALIGNMENT_OFFSET,
+  HEADER_SLOT_SIZE,
+} from '#/components/Layout/const'
+import {ScrollbarOffsetContext} from '#/components/Layout/context'
+import {Text} from '#/components/Typography'
+
+export function Outer({
+  children,
+  noBottomBorder,
+}: {
+  children: React.ReactNode
+  noBottomBorder?: boolean
+}) {
+  const t = useTheme()
+  const gutter = useGutterStyles()
+  const {gtMobile} = useBreakpoints()
+  const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        !noBottomBorder && a.border_b,
+        a.flex_row,
+        a.align_center,
+        a.gap_sm,
+        gutter,
+        platform({
+          native: [a.pb_sm, a.pt_xs],
+          web: [a.py_sm],
+        }),
+        t.atoms.border_contrast_low,
+        gtMobile && [a.mx_auto, {maxWidth: 600}],
+        !isWithinOffsetView && a.scrollbar_offset,
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+const AlignmentContext = createContext<'platform' | 'left'>('platform')
+
+export function Content({
+  children,
+  align = 'platform',
+}: {
+  children?: React.ReactNode
+  align?: 'platform' | 'left'
+}) {
+  return (
+    <View
+      style={[
+        a.flex_1,
+        a.justify_center,
+        isIOS && align === 'platform' && a.align_center,
+        {minHeight: HEADER_SLOT_SIZE},
+      ]}>
+      <AlignmentContext.Provider value={align}>
+        {children}
+      </AlignmentContext.Provider>
+    </View>
+  )
+}
+
+export function Slot({children}: {children?: React.ReactNode}) {
+  return (
+    <View
+      style={[
+        a.z_50,
+        {
+          width: HEADER_SLOT_SIZE,
+        },
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
+
+  const onPressBack = useCallback(
+    (evt: GestureResponderEvent) => {
+      onPress?.(evt)
+      if (evt.defaultPrevented) return
+      if (navigation.canGoBack()) {
+        navigation.goBack()
+      } else {
+        navigation.navigate('Home')
+      }
+    },
+    [onPress, navigation],
+  )
+
+  return (
+    <Slot>
+      <Button
+        label={_(msg`Go back`)}
+        size="small"
+        variant="ghost"
+        color="secondary"
+        shape="square"
+        onPress={onPressBack}
+        hitSlop={HITSLOP_30}
+        style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
+        {...props}>
+        <ButtonIcon icon={ArrowLeft} size="lg" />
+      </Button>
+    </Slot>
+  )
+}
+
+export function MenuButton() {
+  const {_} = useLingui()
+  const setDrawerOpen = useSetDrawerOpen()
+  const {gtMobile} = useBreakpoints()
+
+  const onPress = useCallback(() => {
+    setDrawerOpen(true)
+  }, [setDrawerOpen])
+
+  return gtMobile ? null : (
+    <Slot>
+      <Button
+        label={_(msg`Open drawer menu`)}
+        size="small"
+        variant="ghost"
+        color="secondary"
+        shape="square"
+        onPress={onPress}
+        hitSlop={HITSLOP_30}
+        style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
+        <ButtonIcon icon={Menu} size="lg" />
+      </Button>
+    </Slot>
+  )
+}
+
+export function TitleText({
+  children,
+  style,
+}: {children: React.ReactNode} & TextStyleProp) {
+  const {gtMobile} = useBreakpoints()
+  const align = useContext(AlignmentContext)
+  return (
+    <Text
+      style={[
+        a.text_lg,
+        a.font_heavy,
+        a.leading_tight,
+        isIOS && align === 'platform' && a.text_center,
+        gtMobile && a.text_xl,
+        style,
+      ]}
+      numberOfLines={2}>
+      {children}
+    </Text>
+  )
+}
+
+export function SubtitleText({children}: {children: React.ReactNode}) {
+  const t = useTheme()
+  const align = useContext(AlignmentContext)
+  return (
+    <Text
+      style={[
+        a.text_sm,
+        a.leading_snug,
+        isIOS && align === 'platform' && a.text_center,
+        t.atoms.text_contrast_medium,
+      ]}
+      numberOfLines={2}>
+      {children}
+    </Text>
+  )
+}
diff --git a/src/components/Layout/README.md b/src/components/Layout/README.md
new file mode 100644
index 000000000..1bcc3489e
--- /dev/null
+++ b/src/components/Layout/README.md
@@ -0,0 +1,172 @@
+# Layout
+
+This directory contains our core layout components. Use these when creating new
+screens, or when supplementing other components with functionality like
+centering.
+
+## Usage
+
+If we aren't talking about the `shell` components, layouts on individual screens
+look like more or less like this:
+
+```tsx
+<Outer>
+  <Header>...</Header>
+  <Content>...</Content>
+</Outer>
+```
+
+I'll map these words to real components.
+
+### `Layout.Screen`
+
+Provides the "Outer" functionality for a screen, like taking up the full height
+of the screen. **All screens should be wrapped with this component,** probably
+as the outermost component.
+
+> [!NOTE]
+> On web, `Layout.Screen` also provides the side borders on our central content
+> column. These borders are fixed position, 1px outside our center column width
+> of 600px.
+>
+> What this effectively means is that _nothing inside the center content column
+> needs (or should) define left/right borders._ That is now handled in one
+> place: within `Layout.Screen`.
+
+### `Layout.Header.*`
+
+The `Layout.Header` component actually contains multiple sub-components. Use
+this to compose different versions of the header. The most basic version looks
+like this:
+
+```tsx
+<Layout.Header.Outer>
+  <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
+
+  <Layout.Header.Content>
+    <Layout.Header.TitleText>Account</Layout.Header.TitleText>
+
+    {/* Optional subtitle */}
+    <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
+  </Layout.Header.Content>
+
+  <Layout.Header.Slot />
+</Layout.Header.Outer>
+```
+
+Note the additional `Slot` component. This is here to keep the header balanced
+and provide correct spacing on all platforms. The `Slot` is 34px wide, which
+matches the `BackButton` and `MenuButton`.
+
+> If anyone has better ideas, I'm all ears, but this was simple and the small
+> amount of boilerplate is only incurred when creating a new screen, which is
+> infrequent.
+
+It can also function as a "slot" for a button positioned on the right side. See
+the `Hashtag` screen for an example, abbreviated below:
+
+```tsx
+<Layout.Header.Slot>
+  <Button size='small' shape='round'>...</Button>
+</Layout.Header.Slot>
+```
+
+If you need additional customization, simply use the components that are helpful
+and create new ones as needed. A good example is the `SavedFeeds` screen, which
+looks roughly like this:
+
+```tsx
+<Layout.Header.Outer>
+  <Layout.Header.BackButton />
+
+  {/* Override to align content to the left, making room for the button */}
+  <Layout.Header.Content align='left'>
+    <Layout.Header.TitleText>Edit My Feeds</Layout.Header.TitleText>
+  </Layout.Header.Content>
+
+  {/* Custom button, wider than 34px */}
+  <Button size='small'>...</Button>
+</Layout.Header.Outer>
+```
+
+> [!TIP]
+> The `Header` should be _outside_ the `Content` component in order to be
+> fixed on scroll on native. Placing it inside will make it scroll with the rest
+> of the page.
+
+### `Layout.Content`
+
+This provides the "Content" functionality for a screen. This component is
+actually an `Animated.ScrollView`, and accepts props for that component. It
+provides a little default styling as well. On web, it also _centers the content
+inside our center content column of 600px_.
+
+> [!NOTE]
+> What about flatlists or pagers? Those components are not colocated here (yet).
+> But those components serve the same purpose of "Content".
+
+## Examples
+
+The most basic layout available to us looks like this:
+
+```tsx
+<Layout.Screen>
+  <Layout.Header.Outer>
+    <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
+
+    <Layout.Header.Content>
+      <Layout.Header.TitleText>Account</Layout.Header.TitleText>
+
+      {/* Optional subtitle */}
+      <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
+    </Layout.Header.Content>
+
+    <Layout.Header.Slot />
+  </Layout.Header.Outer>
+
+  <Layout.Content>
+    ...
+  </Layout.Content>
+</Layout.Screen>
+```
+
+**For `List` views,** you'd sub in `List` for `Layout.Content` and it will
+function the same. See `Feeds` screen for an example.
+
+**For `Pager` views,** including `PagerWithHeader`, do the same. See `Hashtag`
+screen for an example.
+
+## Utilities
+
+### `Layout.Center`
+
+This component behaves like our old `CenteredView` component.
+
+### `Layout.SCROLLBAR_OFFSET` and `Layout.SCROLLBAR_OFFSET_POSITIVE`
+
+Provide a pre-configured CSS vars for use when aligning fixed position elements.
+More on this below.
+
+## Scrollbar gutter handling
+
+Operating systems allow users to configure if their browser _always_ shows
+scrollbars not. Some OSs also don't allow configuration.
+
+The presence of scrollbars affects layout, particularly fixed position elements.
+Browsers support `scrollbar-gutter`, but each behaves differently. Our approach
+is to use the default `scrollbar-gutter: auto`. Basically, we start from a clean
+slate.
+
+This handling becomes particularly thorny when we need to lock scroll, like when
+opening a dialog or dropdown. Radix uses the library `react-remove-scroll`
+internally, which in turn depends on
+[`react-remove-scroll-bar`](https://github.com/theKashey/react-remove-scroll-bar).
+We've opted to rely on this transient dependency. This library adds some utility
+classes and CSS vars to the page when scroll is locked.
+
+**It is this CSS variable that we use in `SCROLLBAR_OFFSET` values.** This
+ensures that elements do not shift relative to the screen when opening a
+dropdown or dialog.
+
+These styles are applied where needed and we should have very little need of
+adjusting them often.
diff --git a/src/components/Layout/const.ts b/src/components/Layout/const.ts
new file mode 100644
index 000000000..11825d323
--- /dev/null
+++ b/src/components/Layout/const.ts
@@ -0,0 +1,16 @@
+export const SCROLLBAR_OFFSET =
+  'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any
+export const SCROLLBAR_OFFSET_POSITIVE =
+  'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any
+
+/**
+ * Useful for visually aligning icons within header buttons with the elements
+ * below them on the screen. Apply positively or negatively depending on side
+ * of the screen you're on.
+ */
+export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
+
+/**
+ * Corresponds to the width of a small square or round button
+ */
+export const HEADER_SLOT_SIZE = 34
diff --git a/src/components/Layout/context.ts b/src/components/Layout/context.ts
new file mode 100644
index 000000000..8e0c5445e
--- /dev/null
+++ b/src/components/Layout/context.ts
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export const ScrollbarOffsetContext = React.createContext({
+  isWithinOffsetView: false,
+})
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
new file mode 100644
index 000000000..d08505fbf
--- /dev/null
+++ b/src/components/Layout/index.tsx
@@ -0,0 +1,188 @@
+import React, {useContext, useMemo} from 'react'
+import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native'
+import {StyleProp} from 'react-native'
+import {
+  KeyboardAwareScrollView,
+  KeyboardAwareScrollViewProps,
+} from 'react-native-keyboard-controller'
+import Animated, {
+  AnimatedScrollViewProps,
+  useAnimatedProps,
+} from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+
+import {isWeb} from '#/platform/detection'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {ScrollbarOffsetContext} from '#/components/Layout/context'
+
+export * from '#/components/Layout/const'
+export * as Header from '#/components/Layout/Header'
+
+export type ScreenProps = React.ComponentProps<typeof View> & {
+  style?: StyleProp<ViewStyle>
+}
+
+/**
+ * Outermost component of every screen
+ */
+export const Screen = React.memo(function Screen({
+  style,
+  ...props
+}: ScreenProps) {
+  const {top} = useSafeAreaInsets()
+  return (
+    <>
+      {isWeb && <WebCenterBorders />}
+      <View
+        style={[a.util_screen_outer, {paddingTop: top}, style]}
+        {...props}
+      />
+    </>
+  )
+})
+
+export type ContentProps = AnimatedScrollViewProps & {
+  style?: StyleProp<ViewStyle>
+  contentContainerStyle?: StyleProp<ViewStyle>
+}
+
+/**
+ * Default scroll view for simple pages
+ */
+export const Content = React.memo(function Content({
+  children,
+  style,
+  contentContainerStyle,
+  ...props
+}: ContentProps) {
+  const {footerHeight} = useShellLayout()
+  const animatedProps = useAnimatedProps(() => {
+    return {
+      scrollIndicatorInsets: {
+        bottom: footerHeight.get(),
+        top: 0,
+        right: 1,
+      },
+    } satisfies AnimatedScrollViewProps
+  })
+
+  return (
+    <Animated.ScrollView
+      id="content"
+      automaticallyAdjustsScrollIndicatorInsets={false}
+      // sets the scroll inset to the height of the footer
+      animatedProps={animatedProps}
+      style={[scrollViewStyles.common, style]}
+      contentContainerStyle={[
+        scrollViewStyles.contentContainer,
+        contentContainerStyle,
+      ]}
+      {...props}>
+      {isWeb ? (
+        // @ts-ignore web only -esb
+        <Center>{children}</Center>
+      ) : (
+        children
+      )}
+    </Animated.ScrollView>
+  )
+})
+
+const scrollViewStyles = StyleSheet.create({
+  common: {
+    width: '100%',
+  },
+  contentContainer: {
+    paddingBottom: 100,
+  },
+})
+
+export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
+  children: React.ReactNode
+  contentContainerStyle?: StyleProp<ViewStyle>
+}
+
+/**
+ * Default scroll view for simple pages.
+ *
+ * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
+ */
+export const KeyboardAwareContent = React.memo(function LayoutScrollView({
+  children,
+  style,
+  contentContainerStyle,
+  ...props
+}: KeyboardAwareContentProps) {
+  return (
+    <KeyboardAwareScrollView
+      style={[scrollViewStyles.common, style]}
+      contentContainerStyle={[
+        scrollViewStyles.contentContainer,
+        contentContainerStyle,
+      ]}
+      keyboardShouldPersistTaps="handled"
+      {...props}>
+      {isWeb ? <Center>{children}</Center> : children}
+    </KeyboardAwareScrollView>
+  )
+})
+
+/**
+ * Utility component to center content within the screen
+ */
+export const Center = React.memo(function LayoutContent({
+  children,
+  style,
+  ...props
+}: ViewProps) {
+  const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
+  const {gtMobile} = useBreakpoints()
+  const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.mx_auto,
+        gtMobile && {
+          maxWidth: 600,
+        },
+        style,
+        !isWithinOffsetView && a.scrollbar_offset,
+      ]}
+      {...props}>
+      <ScrollbarOffsetContext.Provider value={ctx}>
+        {children}
+      </ScrollbarOffsetContext.Provider>
+    </View>
+  )
+})
+
+/**
+ * Only used within `Layout.Screen`, not for reuse
+ */
+const WebCenterBorders = React.memo(function LayoutContent() {
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  return gtMobile ? (
+    <View
+      style={[
+        a.fixed,
+        a.inset_0,
+        a.border_l,
+        a.border_r,
+        t.atoms.border_contrast_low,
+        web({
+          width: 602,
+          left: '50%',
+          transform: [
+            {
+              translateX: '-50%',
+            },
+            ...a.scrollbar_offset.transform,
+          ],
+        }),
+      ]}
+    />
+  ) : null
+})
diff --git a/src/components/LikedByList.tsx b/src/components/LikedByList.tsx
index a83f98258..b369bd76e 100644
--- a/src/components/LikedByList.tsx
+++ b/src/components/LikedByList.tsx
@@ -12,8 +12,14 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {List} from '#/view/com/util/List'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
-function renderItem({item}: {item: GetLikes.Like}) {
-  return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
+function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
+  return (
+    <ProfileCardWithFollowBtn
+      key={item.actor.did}
+      profile={item.actor}
+      noBorder={index === 0}
+    />
+  )
 }
 
 function keyExtractor(item: GetLikes.Like) {
@@ -81,6 +87,8 @@ export function LikedByList({uri}: {uri: string}) {
         )}
         errorMessage={cleanError(resolveError || error)}
         onRetry={isError ? refetch : undefined}
+        topBorder={false}
+        sideBorders={false}
       />
     )
   }
@@ -103,6 +111,7 @@ export function LikedByList({uri}: {uri: string}) {
       onEndReachedThreshold={3}
       initialNumToRender={initialNumToRender}
       windowSize={11}
+      sideBorders={false}
     />
   )
 }
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 16bd6a9ea..2d7b13b25 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -109,38 +109,6 @@ function ListFooterMaybeError({
   )
 }
 
-export function ListHeaderDesktop({
-  title,
-  subtitle,
-}: {
-  title: string
-  subtitle?: string
-}) {
-  const {gtTablet} = useBreakpoints()
-  const t = useTheme()
-
-  if (!gtTablet) return null
-
-  return (
-    <View
-      style={[
-        a.w_full,
-        a.py_sm,
-        a.px_xl,
-        a.gap_xs,
-        a.justify_center,
-        {minHeight: 50},
-      ]}>
-      <Text style={[a.text_2xl, a.font_bold]}>{title}</Text>
-      {subtitle ? (
-        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
-          {subtitle}
-        </Text>
-      ) : undefined}
-    </View>
-  )
-}
-
 let ListMaybePlaceholder = ({
   isLoading,
   noEmpty,
@@ -154,7 +122,7 @@ let ListMaybePlaceholder = ({
   onGoBack,
   hideBackButton,
   sideBorders,
-  topBorder = true,
+  topBorder = false,
 }: {
   isLoading: boolean
   noEmpty?: boolean
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index 6c3bbf216..acffa0c2b 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -65,25 +65,23 @@ export let MessagesListHeader = ({
         a.pr_lg,
         a.py_sm,
       ]}>
-      {!gtTablet && (
-        <TouchableOpacity
-          testID="conversationHeaderBackBtn"
-          onPress={onPressBack}
-          hitSlop={BACK_HITSLOP}
-          style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Back`)}
-          accessibilityHint="">
-          <FontAwesomeIcon
-            size={18}
-            icon="angle-left"
-            style={{
-              marginTop: 6,
-            }}
-            color={t.atoms.text.color}
-          />
-        </TouchableOpacity>
-      )}
+      <TouchableOpacity
+        testID="conversationHeaderBackBtn"
+        onPress={onPressBack}
+        hitSlop={BACK_HITSLOP}
+        style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Back`)}
+        accessibilityHint="">
+        <FontAwesomeIcon
+          size={18}
+          icon="angle-left"
+          style={{
+            marginTop: 6,
+          }}
+          color={t.atoms.text.color}
+        />
+      </TouchableOpacity>
 
       {profile && moderation && blockInfo ? (
         <HeaderReady
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx
index b64c0e9fa..58f4d4f89 100644
--- a/src/components/forms/DateField/index.android.tsx
+++ b/src/components/forms/DateField/index.android.tsx
@@ -57,6 +57,7 @@ export function DateField({
           open
           timeZoneOffsetInMinutes={0}
           theme={t.scheme}
+          // @ts-ignore TODO
           buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
           date={new Date(value)}
           onConfirm={onChangeInternal}
diff --git a/src/components/icons/FloppyDisk.tsx b/src/components/icons/FloppyDisk.tsx
new file mode 100644
index 000000000..7fb938089
--- /dev/null
+++ b/src/components/icons/FloppyDisk.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z',
+})
diff --git a/src/lib/hooks/useWebBodyScrollLock.ts b/src/lib/hooks/useWebBodyScrollLock.ts
deleted file mode 100644
index c63c23b29..000000000
--- a/src/lib/hooks/useWebBodyScrollLock.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {useEffect} from 'react'
-
-import {isWeb} from '#/platform/detection'
-
-let refCount = 0
-
-function incrementRefCount() {
-  if (refCount === 0) {
-    document.body.style.overflow = 'hidden'
-    document.documentElement.style.scrollbarGutter = 'auto'
-  }
-  refCount++
-}
-
-function decrementRefCount() {
-  refCount--
-  if (refCount === 0) {
-    document.body.style.overflow = ''
-    document.documentElement.style.scrollbarGutter = ''
-  }
-}
-
-export function useWebBodyScrollLock(isLockActive: boolean) {
-  useEffect(() => {
-    if (!isWeb || !isLockActive) {
-      return
-    }
-    incrementRefCount()
-    return () => decrementRefCount()
-  })
-}
diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx
index 36b96cacd..4fcb42854 100644
--- a/src/screens/Deactivated.tsx
+++ b/src/screens/Deactivated.tsx
@@ -17,13 +17,13 @@ import {
 } from '#/state/session'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import {ScrollView} from '#/view/com/util/Views'
 import {Logo} from '#/view/icons/Logo'
 import {atoms as a, useTheme} from '#/alf'
 import {AccountList} from '#/components/AccountList'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import * as Layout from '#/components/Layout'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 
@@ -104,24 +104,17 @@ export function Deactivated() {
   }, [_, agent, setPending, setError, queryClient])
 
   return (
-    <View style={[a.util_screen_outer, a.flex_1, t.atoms.bg]}>
-      <ScrollView
-        style={[
-          a.h_full,
-          a.w_full,
+    <View style={[a.util_screen_outer, a.flex_1]}>
+      <Layout.Content
+        contentContainerStyle={[
           a.px_2xl,
           {
             paddingTop: isWeb ? 64 : insets.top + 16,
             paddingBottom: isWeb ? 64 : insets.bottom,
           },
-        ]}
-        contentContainerStyle={[
-          a.w_full,
-          a.flex_row,
-          a.justify_center,
-          {borderWidth: 0},
         ]}>
-        <View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
+        <View
+          style={[a.w_full, {marginHorizontal: 'auto', maxWidth: COL_WIDTH}]}>
           <View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}>
             <Logo width={40} />
           </View>
@@ -218,7 +211,7 @@ export function Deactivated() {
             </>
           )}
         </View>
-      </ScrollView>
+      </Layout.Content>
     </View>
   )
 }
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
index adf5f0080..a0fc3707c 100644
--- a/src/screens/Hashtag.tsx
+++ b/src/screens/Hashtag.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {ListRenderItemInfo, Pressable, View} from 'react-native'
+import {ListRenderItemInfo, View} from 'react-native'
 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -13,16 +13,15 @@ import {shareUrl} from '#/lib/sharing'
 import {cleanError} from '#/lib/strings/errors'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {enforceLen} from '#/lib/strings/helpers'
-import {isNative, isWeb} from '#/platform/detection'
 import {useSearchPostsQuery} from '#/state/queries/search-posts'
 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
 import {Pager} from '#/view/com/pager/Pager'
 import {TabBar} from '#/view/com/pager/TabBar'
 import {Post} from '#/view/com/post/Post'
 import {List} from '#/view/com/util/List'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
+import {atoms as a, web} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
 import * as Layout from '#/components/Layout'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
@@ -110,46 +109,36 @@ export default function HashtagScreen({
 
   return (
     <Layout.Screen>
-      <CenteredView sideBorders={true}>
-        <ViewHeader
-          showOnDesktop
-          title={headerTitle}
-          subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
-          canGoBack
-          renderButton={
-            isNative
-              ? () => (
-                  <Pressable
-                    accessibilityRole="button"
-                    onPress={onShare}
-                    hitSlop={HITSLOP_10}>
-                    <ArrowOutOfBox_Stroke2_Corner0_Rounded
-                      size="lg"
-                      onPress={onShare}
-                    />
-                  </Pressable>
-                )
-              : undefined
-          }
-        />
-      </CenteredView>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
+          {author && (
+            <Layout.Header.SubtitleText>
+              {_(msg`From @${sanitizedAuthor}`)}
+            </Layout.Header.SubtitleText>
+          )}
+        </Layout.Header.Content>
+        <Layout.Header.Slot>
+          <Button
+            label={_(msg`Share`)}
+            size="small"
+            variant="ghost"
+            color="primary"
+            shape="round"
+            onPress={onShare}
+            hitSlop={HITSLOP_10}
+            style={[{right: -3}]}>
+            <ButtonIcon icon={Share} size="md" />
+          </Button>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
       <Pager
         onPageSelected={onPageSelected}
         renderTabBar={props => (
-          <CenteredView
-            sideBorders={true}
-            // @ts-ignore web only
-            style={
-              isWeb
-                ? {
-                    position: isWeb ? 'sticky' : '',
-                    top: 0,
-                    zIndex: 1,
-                  }
-                : undefined
-            }>
+          <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}>
             <TabBar items={sections.map(section => section.title)} {...props} />
-          </CenteredView>
+          </Layout.Center>
         )}
         initialPage={0}>
         {sections.map((section, i) => (
diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx
index 4f2bd251f..1a87a2ac5 100644
--- a/src/screens/Messages/ChatList.tsx
+++ b/src/screens/Messages/ChatList.tsx
@@ -16,8 +16,6 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
 import {useMessagesEventBus} from '#/state/messages/events'
 import {useListConvosQuery} from '#/state/queries/messages/list-converations'
 import {List} from '#/view/com/util/List'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {DialogControlProps, useDialogControl} from '#/components/Dialog'
@@ -49,7 +47,6 @@ export function MessagesScreen({navigation, route}: Props) {
   const {_} = useLingui()
   const t = useTheme()
   const newChatControl = useDialogControl()
-  const {gtMobile} = useBreakpoints()
   const pushToConversation = route.params?.pushToConversation
 
   // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on
@@ -81,21 +78,6 @@ export function MessagesScreen({navigation, route}: Props) {
     }, [messagesBus, isActive]),
   )
 
-  const renderButton = useCallback(() => {
-    return (
-      <Link
-        to="/messages/settings"
-        label={_(msg`Chat settings`)}
-        size="small"
-        variant="ghost"
-        color="secondary"
-        shape="square"
-        style={[a.justify_center]}>
-        <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} />
-      </Link>
-    )
-  }, [_, t])
-
   const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
   const [isPTRing, setIsPTRing] = useState(false)
 
@@ -144,28 +126,11 @@ export function MessagesScreen({navigation, route}: Props) {
     [navigation],
   )
 
-  const onNavigateToSettings = useCallback(() => {
-    navigation.navigate('MessagesSettings')
-  }, [navigation])
-
   if (conversations.length < 1) {
     return (
       <Layout.Screen>
-        <CenteredView sideBorders={gtMobile} style={[a.h_full_vh]}>
-          {gtMobile ? (
-            <DesktopHeader
-              newChatControl={newChatControl}
-              onNavigateToSettings={onNavigateToSettings}
-            />
-          ) : (
-            <ViewHeader
-              title={_(msg`Messages`)}
-              renderButton={renderButton}
-              showBorder
-              canGoBack={false}
-            />
-          )}
-
+        <Header newChatControl={newChatControl} />
+        <Layout.Center>
           {isLoading ? (
             <View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}>
               <Loader size="xl" />
@@ -227,7 +192,7 @@ export function MessagesScreen({navigation, route}: Props) {
               )}
             </>
           )}
-        </CenteredView>
+        </Layout.Center>
 
         {!isLoading && !isError && (
           <NewChat onNewChat={onNewChat} control={newChatControl} />
@@ -238,14 +203,7 @@ export function MessagesScreen({navigation, route}: Props) {
 
   return (
     <Layout.Screen testID="messagesScreen">
-      {!gtMobile && (
-        <ViewHeader
-          title={_(msg`Messages`)}
-          renderButton={renderButton}
-          showBorder
-          canGoBack={false}
-        />
-      )}
+      <Header newChatControl={newChatControl} />
       <NewChat onNewChat={onNewChat} control={newChatControl} />
       <List
         data={conversations}
@@ -254,12 +212,6 @@ export function MessagesScreen({navigation, route}: Props) {
         refreshing={isPTRing}
         onRefresh={onRefresh}
         onEndReached={onEndReached}
-        ListHeaderComponent={
-          <DesktopHeader
-            newChatControl={newChatControl}
-            onNavigateToSettings={onNavigateToSettings}
-          />
-        }
         ListFooterComponent={
           <ListFooter
             isFetchingNextPage={isFetchingNextPage}
@@ -276,67 +228,65 @@ export function MessagesScreen({navigation, route}: Props) {
         windowSize={11}
         // @ts-ignore our .web version only -sfn
         desktopFixedHeight
+        sideBorders={false}
       />
     </Layout.Screen>
   )
 }
 
-function DesktopHeader({
-  newChatControl,
-  onNavigateToSettings,
-}: {
-  newChatControl: DialogControlProps
-  onNavigateToSettings: () => void
-}) {
-  const t = useTheme()
+function Header({newChatControl}: {newChatControl: DialogControlProps}) {
   const {_} = useLingui()
-  const {gtMobile, gtTablet} = useBreakpoints()
+  const {gtMobile} = useBreakpoints()
 
-  if (!gtMobile) {
-    return null
-  }
+  const settingsLink = (
+    <Link
+      to="/messages/settings"
+      label={_(msg`Chat settings`)}
+      size="small"
+      variant="ghost"
+      color="secondary"
+      shape="square"
+      style={[a.justify_center]}>
+      <ButtonIcon icon={SettingsSlider} size="md" />
+    </Link>
+  )
 
   return (
-    <View
-      style={[
-        t.atoms.bg,
-        a.flex_row,
-        a.align_center,
-        a.justify_between,
-        a.gap_lg,
-        a.px_lg,
-        a.pr_md,
-        a.py_sm,
-        a.border_b,
-        t.atoms.border_contrast_low,
-      ]}>
-      <Text style={[a.text_2xl, a.font_bold]}>
-        <Trans>Messages</Trans>
-      </Text>
-      <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-        <Button
-          label={_(msg`Message settings`)}
-          color="secondary"
-          size="small"
-          variant="ghost"
-          shape="square"
-          onPress={onNavigateToSettings}>
-          <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} />
-        </Button>
-        {gtTablet && (
-          <Button
-            label={_(msg`New chat`)}
-            color="primary"
-            size="small"
-            variant="solid"
-            onPress={newChatControl.open}>
-            <ButtonIcon icon={Plus} position="left" />
-            <ButtonText>
-              <Trans>New chat</Trans>
-            </ButtonText>
-          </Button>
-        )}
-      </View>
-    </View>
+    <Layout.Header.Outer>
+      {gtMobile ? (
+        <>
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Messages</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+
+          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
+            {settingsLink}
+            <Button
+              label={_(msg`New chat`)}
+              color="primary"
+              size="small"
+              variant="solid"
+              onPress={newChatControl.open}>
+              <ButtonIcon icon={Plus} position="left" />
+              <ButtonText>
+                <Trans>New chat</Trans>
+              </ButtonText>
+            </Button>
+          </View>
+        </>
+      ) : (
+        <>
+          <Layout.Header.MenuButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Messages</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot>{settingsLink}</Layout.Header.Slot>
+        </>
+      )}
+    </Layout.Header.Outer>
   )
 }
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index a2157d2b9..b8b0bfe0d 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -17,7 +17,6 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useSetMinimalShellMode} from '#/state/shell'
-import {CenteredView} from '#/view/com/util/Views'
 import {MessagesList} from '#/screens/Messages/components/MessagesList'
 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
 import {useDialogControl} from '#/components/Dialog'
@@ -97,7 +96,7 @@ function Inner() {
 
   if (convoState.status === ConvoStatus.Error) {
     return (
-      <CenteredView style={[a.flex_1]} sideBorders>
+      <Layout.Center style={[a.flex_1]}>
         <MessagesListHeader />
         <Error
           title={_(msg`Something went wrong`)}
@@ -105,12 +104,12 @@ function Inner() {
           onRetry={() => convoState.error.retry()}
           sideBorders={false}
         />
-      </CenteredView>
+      </Layout.Center>
     )
   }
 
   return (
-    <CenteredView style={[a.flex_1]} sideBorders>
+    <Layout.Center style={[a.flex_1]}>
       {!readyToShow && <MessagesListHeader />}
       <View style={[a.flex_1]}>
         {moderationOpts && recipient ? (
@@ -140,7 +139,7 @@ function Inner() {
           </View>
         )}
       </View>
-    </CenteredView>
+    </Layout.Center>
   )
 }
 
diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx
index 50b1c4cc9..f37e7a9ba 100644
--- a/src/screens/Messages/Settings.tsx
+++ b/src/screens/Messages/Settings.tsx
@@ -10,8 +10,6 @@ import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declarat
 import {useProfileQuery} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
 import {atoms as a} from '#/alf'
 import {Admonition} from '#/components/Admonition'
 import {Divider} from '#/components/Divider'
@@ -57,8 +55,16 @@ export function MessagesSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen testID="messagesSettingsScreen">
-      <ScrollView stickyHeaderIndices={[0]}>
-        <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Chat Settings</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content>
         <View style={[a.p_lg, a.gap_md]}>
           <Text style={[a.text_lg, a.font_bold]}>
             <Trans>Allow new messages from</Trans>
@@ -142,7 +148,7 @@ export function MessagesSettingsScreen({}: Props) {
             </>
           )}
         </View>
-      </ScrollView>
+      </Layout.Content>
     </Layout.Screen>
   )
 }
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 5f340cd56..6b4dd06bc 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -1,6 +1,5 @@
-import React from 'react'
+import {Fragment, useCallback} from 'react'
 import {Linking, View} from 'react-native'
-import {useSafeAreaFrame} from 'react-native-safe-area-context'
 import {LABELS} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -19,8 +18,6 @@ import {
 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
-import {ScrollView} from '#/view/com/util/Views'
 import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
@@ -37,6 +34,7 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso
 import * as LabelingService from '#/components/LabelingServiceCard'
 import * as Layout from '#/components/Layout'
 import {InlineLinkText, Link} from '#/components/Link'
+import {ListMaybePlaceholder} from '#/components/Lists'
 import {Loader} from '#/components/Loader'
 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
 import {Text} from '#/components/Typography'
@@ -75,35 +73,22 @@ function ErrorState({error}: {error: string}) {
 export function ModerationScreen(
   _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
 ) {
-  const t = useTheme()
   const {_} = useLingui()
   const {
     isLoading: isPreferencesLoading,
     error: preferencesError,
     data: preferences,
   } = usePreferencesQuery()
-  const {gtMobile} = useBreakpoints()
-  const {height} = useSafeAreaFrame()
 
   const isLoading = isPreferencesLoading
   const error = preferencesError
 
   return (
     <Layout.Screen testID="moderationScreen">
-      <CenteredView
-        testID="moderationScreen"
-        style={[
-          t.atoms.border_contrast_low,
-          t.atoms.bg,
-          {minHeight: height},
-          ...(gtMobile ? [a.border_l, a.border_r] : []),
-        ]}>
-        <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
-
+      <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
+      <Layout.Content>
         {isLoading ? (
-          <View style={[a.w_full, a.align_center, a.pt_2xl]}>
-            <Loader size="xl" fill={t.atoms.text.color} />
-          </View>
+          <ListMaybePlaceholder isLoading={true} sideBorders={false} />
         ) : error || !preferences ? (
           <ErrorState
             error={
@@ -114,7 +99,7 @@ export function ModerationScreen(
         ) : (
           <ModerationScreenInner preferences={preferences} />
         )}
-      </CenteredView>
+      </Layout.Content>
     </Layout.Screen>
   )
 }
@@ -169,7 +154,7 @@ export function ModerationScreenInner({
   } = useMyLabelersQuery()
 
   useFocusEffect(
-    React.useCallback(() => {
+    useCallback(() => {
       setMinimalShellMode(false)
     }, [setMinimalShellMode]),
   )
@@ -183,7 +168,7 @@ export function ModerationScreenInner({
   const ageNotSet = !preferences.userAge
   const isUnderage = (preferences.userAge || 0) < 18
 
-  const onToggleAdultContentEnabled = React.useCallback(
+  const onToggleAdultContentEnabled = useCallback(
     async (selected: boolean) => {
       try {
         await setAdultContentPref({
@@ -201,13 +186,7 @@ export function ModerationScreenInner({
   const disabledOnIOS = isIOS && !adultContentEnabled
 
   return (
-    <ScrollView
-      contentContainerStyle={[
-        a.border_0,
-        a.pt_2xl,
-        a.px_lg,
-        gtMobile && a.px_2xl,
-      ]}>
+    <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}>
       <Text
         style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
         <Trans>Moderation tools</Trans>
@@ -420,7 +399,7 @@ export function ModerationScreenInner({
         <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}>
           {labelers.map((labeler, i) => {
             return (
-              <React.Fragment key={labeler.creator.did}>
+              <Fragment key={labeler.creator.did}>
                 {i !== 0 && <Divider />}
                 <LabelingService.Link labeler={labeler}>
                   {state => (
@@ -457,12 +436,12 @@ export function ModerationScreenInner({
                     </LabelingService.Outer>
                   )}
                 </LabelingService.Link>
-              </React.Fragment>
+              </Fragment>
             )
           })}
         </View>
       )}
-      <View style={{height: 200}} />
-    </ScrollView>
+      <View style={{height: 150}} />
+    </View>
   )
 }
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx
index 54821532c..059cdfd5c 100644
--- a/src/screens/Onboarding/Layout.tsx
+++ b/src/screens/Onboarding/Layout.tsx
@@ -1,13 +1,11 @@
 import React from 'react'
-import {View} from 'react-native'
-import Animated from 'react-native-reanimated'
+import {ScrollView, View} from 'react-native'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {isWeb} from '#/platform/detection'
 import {useOnboardingDispatch} from '#/state/shell'
-import {ScrollView} from '#/view/com/util/Views'
 import {Context} from '#/screens/Onboarding/state'
 import {
   atoms as a,
@@ -36,7 +34,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
   const {gtMobile} = useBreakpoints()
   const onboardDispatch = useOnboardingDispatch()
   const {state, dispatch} = React.useContext(Context)
-  const scrollview = React.useRef<Animated.ScrollView>(null)
+  const scrollview = React.useRef<ScrollView>(null)
   const prevActiveStep = React.useRef<string>(state.activeStep)
 
   React.useEffect(() => {
diff --git a/src/screens/Post/PostLikedBy.tsx b/src/screens/Post/PostLikedBy.tsx
index 6fc485f34..d35d33243 100644
--- a/src/screens/Post/PostLikedBy.tsx
+++ b/src/screens/Post/PostLikedBy.tsx
@@ -5,13 +5,10 @@ import {useFocusEffect} from '@react-navigation/native'
 
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {isWeb} from '#/platform/detection'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
-import {ListHeaderDesktop} from '#/components/Lists'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
 export const PostLikedByScreen = ({route}: Props) => {
@@ -28,11 +25,8 @@ export const PostLikedByScreen = ({route}: Props) => {
 
   return (
     <Layout.Screen>
-      <CenteredView sideBorders={true}>
-        <ListHeaderDesktop title={_(msg`Liked By`)} />
-        <ViewHeader title={_(msg`Liked By`)} showBorder={!isWeb} />
-        <PostLikedByComponent uri={uri} />
-      </CenteredView>
+      <ViewHeader title={_(msg`Liked By`)} />
+      <PostLikedByComponent uri={uri} />
     </Layout.Screen>
   )
 }
diff --git a/src/screens/Post/PostQuotes.tsx b/src/screens/Post/PostQuotes.tsx
index 71dd8ad8d..2cd6be879 100644
--- a/src/screens/Post/PostQuotes.tsx
+++ b/src/screens/Post/PostQuotes.tsx
@@ -11,7 +11,6 @@ import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuot
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
-import {ListHeaderDesktop} from '#/components/Lists'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
 export const PostQuotesScreen = ({route}: Props) => {
@@ -29,7 +28,6 @@ export const PostQuotesScreen = ({route}: Props) => {
   return (
     <Layout.Screen>
       <CenteredView sideBorders={true}>
-        <ListHeaderDesktop title={_(msg`Quotes`)} />
         <ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
         <PostQuotesComponent uri={uri} />
       </CenteredView>
diff --git a/src/screens/Post/PostRepostedBy.tsx b/src/screens/Post/PostRepostedBy.tsx
index c1e8b2987..304e70808 100644
--- a/src/screens/Post/PostRepostedBy.tsx
+++ b/src/screens/Post/PostRepostedBy.tsx
@@ -11,7 +11,6 @@ import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
-import {ListHeaderDesktop} from '#/components/Lists'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
 export const PostRepostedByScreen = ({route}: Props) => {
@@ -29,7 +28,6 @@ export const PostRepostedByScreen = ({route}: Props) => {
   return (
     <Layout.Screen>
       <CenteredView sideBorders={true}>
-        <ListHeaderDesktop title={_(msg`Reposted By`)} />
         <ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
         <PostRepostedByComponent uri={uri} />
       </CenteredView>
diff --git a/src/screens/Profile/KnownFollowers.tsx b/src/screens/Profile/KnownFollowers.tsx
index 7e396c350..d6dd15c69 100644
--- a/src/screens/Profile/KnownFollowers.tsx
+++ b/src/screens/Profile/KnownFollowers.tsx
@@ -15,14 +15,22 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {List} from '#/view/com/util/List'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import * as Layout from '#/components/Layout'
-import {
-  ListFooter,
-  ListHeaderDesktop,
-  ListMaybePlaceholder,
-} from '#/components/Lists'
+import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
-function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) {
-  return <ProfileCardWithFollowBtn key={item.did} profile={item} />
+function renderItem({
+  item,
+  index,
+}: {
+  item: AppBskyActorDefs.ProfileViewBasic
+  index: number
+}) {
+  return (
+    <ProfileCardWithFollowBtn
+      key={item.did}
+      profile={item}
+      noBorder={index === 0}
+    />
+  )
 }
 
 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
@@ -93,6 +101,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
   if (followers.length < 1) {
     return (
       <Layout.Screen>
+        <ViewHeader title={_(msg`Followers you know`)} />
         <ListMaybePlaceholder
           isLoading={isDidLoading || isFollowersLoading}
           isError={isError}
@@ -100,6 +109,8 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
           emptyMessage={_(msg`You don't follow any users who follow @${name}.`)}
           errorMessage={cleanError(resolveError || error)}
           onRetry={isError ? refetch : undefined}
+          topBorder={false}
+          sideBorders={false}
         />
       </Layout.Screen>
     )
@@ -116,9 +127,6 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
         onRefresh={onRefresh}
         onEndReached={onEndReached}
         onEndReachedThreshold={4}
-        ListHeaderComponent={
-          <ListHeaderDesktop title={_(msg`Followers you know`)} />
-        }
         ListFooterComponent={
           <ListFooter
             isFetchingNextPage={isFetchingNextPage}
@@ -130,6 +138,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
         desktopFixedHeight
         initialNumToRender={initialNumToRender}
         windowSize={11}
+        sideBorders={false}
       />
     </Layout.Screen>
   )
diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx
index 67c827d90..6c76d7b15 100644
--- a/src/screens/Profile/Sections/Labels.tsx
+++ b/src/screens/Profile/Sections/Labels.tsx
@@ -15,10 +15,11 @@ import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
 import {useScrollHandlers} from '#/lib/ScrollContext'
 import {isNative} from '#/platform/detection'
 import {ListRef} from '#/view/com/util/List'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {ScrollView} from '#/view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
 import {Divider} from '#/components/Divider'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import * as Layout from '#/components/Layout'
 import {Loader} from '#/components/Loader'
 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
 import {Text} from '#/components/Typography'
@@ -75,7 +76,7 @@ export const ProfileLabelsSection = React.forwardRef<
   }, [isFocused, scrollElRef, setScrollViewTag])
 
   return (
-    <CenteredView style={{flex: 1, minHeight}} sideBorders>
+    <Layout.Center style={{flex: 1, minHeight}}>
       {isLabelerLoading ? (
         <View style={[a.w_full, a.align_center]}>
           <Loader size="xl" />
@@ -95,7 +96,7 @@ export const ProfileLabelsSection = React.forwardRef<
           headerHeight={headerHeight}
         />
       )}
-    </CenteredView>
+    </Layout.Center>
   )
 })
 
diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx
index 8019a20f9..02976bb3c 100644
--- a/src/screens/Settings/AboutSettings.tsx
+++ b/src/screens/Settings/AboutSettings.tsx
@@ -21,7 +21,15 @@ export function AboutSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`About`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>About</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.LinkItem
diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx
index 6ab0131d9..ee26697d2 100644
--- a/src/screens/Settings/AccessibilitySettings.tsx
+++ b/src/screens/Settings/AccessibilitySettings.tsx
@@ -39,7 +39,15 @@ export function AccessibilitySettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Accessibility`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Accessibility</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx
index 2495a0f2f..634c9d3f7 100644
--- a/src/screens/Settings/AccountSettings.tsx
+++ b/src/screens/Settings/AccountSettings.tsx
@@ -38,7 +38,15 @@ export function AccountSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Account`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Account</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Item>
diff --git a/src/screens/Settings/AppIconSettings.tsx b/src/screens/Settings/AppIconSettings.tsx
index 1dd87d45f..18fcd5e30 100644
--- a/src/screens/Settings/AppIconSettings.tsx
+++ b/src/screens/Settings/AppIconSettings.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {Alert, View} from 'react-native'
 import {Image} from 'expo-image'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -20,7 +20,15 @@ export function AppIconSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_('App Icon')} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>App Icon</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content
         contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
         <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx
index 1ea0bd1b3..630d26ba7 100644
--- a/src/screens/Settings/AppPasswords.tsx
+++ b/src/screens/Settings/AppPasswords.tsx
@@ -44,7 +44,15 @@ export function AppPasswordsScreen({}: Props) {
 
   return (
     <Layout.Screen testID="AppPasswordsScreen">
-      <Layout.Header title={_(msg`App Passwords`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>App Passwords</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         {error ? (
           <ErrorScreen
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index 82c4ef97e..48c4a2d85 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -79,7 +79,15 @@ export function AppearanceSettingsScreen({}: Props) {
   return (
     <LayoutAnimationConfig skipExiting skipEntering>
       <Layout.Screen testID="preferencesThreadsScreen">
-        <Layout.Header title={_(msg`Appearance`)} />
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Appearance</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot />
+        </Layout.Header.Outer>
         <Layout.Content>
           <SettingsList.Container>
             <AppearanceToggleButtonGroup
diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx
index b3fb8c174..17f8fa506 100644
--- a/src/screens/Settings/ContentAndMediaSettings.tsx
+++ b/src/screens/Settings/ContentAndMediaSettings.tsx
@@ -32,7 +32,15 @@ export function ContentAndMediaSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Content and Media`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Content & Media</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.LinkItem
diff --git a/src/screens/Settings/ExternalMediaPreferences.tsx b/src/screens/Settings/ExternalMediaPreferences.tsx
index f7e081429..ae859295f 100644
--- a/src/screens/Settings/ExternalMediaPreferences.tsx
+++ b/src/screens/Settings/ExternalMediaPreferences.tsx
@@ -1,7 +1,6 @@
 import {Fragment} from 'react'
 import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {
@@ -23,10 +22,17 @@ type Props = NativeStackScreenProps<
   'PreferencesExternalEmbeds'
 >
 export function ExternalMediaPreferencesScreen({}: Props) {
-  const {_} = useLingui()
   return (
     <Layout.Screen testID="externalMediaPreferencesScreen">
-      <Layout.Header title={_(msg`External Media Preferences`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>External Media Preferences</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Item>
diff --git a/src/screens/Settings/FollowingFeedPreferences.tsx b/src/screens/Settings/FollowingFeedPreferences.tsx
index 089491dd0..ea9455ab1 100644
--- a/src/screens/Settings/FollowingFeedPreferences.tsx
+++ b/src/screens/Settings/FollowingFeedPreferences.tsx
@@ -46,7 +46,15 @@ export function FollowingFeedPreferencesScreen({}: Props) {
 
   return (
     <Layout.Screen testID="followingFeedPreferencesScreen">
-      <Layout.Header title={_(msg`Following Feed Preferences`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Following Feed Preferences</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Item>
diff --git a/src/screens/Settings/LanguageSettings.tsx b/src/screens/Settings/LanguageSettings.tsx
index a44e2fcec..096f92566 100644
--- a/src/screens/Settings/LanguageSettings.tsx
+++ b/src/screens/Settings/LanguageSettings.tsx
@@ -64,7 +64,15 @@ export function LanguageSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen testID="PreferencesLanguagesScreen">
-      <Layout.Header title={_(msg`Languages`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Languages</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Group iconInset={false}>
diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx
index c5f7078c4..1c77b3148 100644
--- a/src/screens/Settings/NotificationSettings.tsx
+++ b/src/screens/Settings/NotificationSettings.tsx
@@ -33,7 +33,15 @@ export function NotificationSettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Notification Settings`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Notification Settings</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         {isQueryError ? (
           <Error
diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
index d695f830d..870ece4bf 100644
--- a/src/screens/Settings/PrivacyAndSecuritySettings.tsx
+++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
@@ -29,7 +29,15 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Privacy and Security`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Privacy and Security</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Item>
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index 126a1bc88..7a4ad6f20 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -73,7 +73,15 @@ export function SettingsScreen({}: Props) {
 
   return (
     <Layout.Screen>
-      <Layout.Header title={_(msg`Settings`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Settings</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <View
diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx
index d29daa58b..b1547e495 100644
--- a/src/screens/Settings/ThreadPreferences.tsx
+++ b/src/screens/Settings/ThreadPreferences.tsx
@@ -38,7 +38,15 @@ export function ThreadPreferencesScreen({}: Props) {
 
   return (
     <Layout.Screen testID="threadPreferencesScreen">
-      <Layout.Header title={_(msg`Thread Preferences`)} />
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Thread Preferences</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
       <Layout.Content>
         <SettingsList.Container>
           <SettingsList.Group>
diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx
index ed261f29e..f1c36a69c 100644
--- a/src/screens/SignupQueued.tsx
+++ b/src/screens/SignupQueued.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Modal, View} from 'react-native'
+import {Modal, ScrollView, View} from 'react-native'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {StatusBar} from 'expo-status-bar'
 import {msg, plural, Trans} from '@lingui/macro'
@@ -9,7 +9,6 @@ import {logger} from '#/logger'
 import {isIOS, isWeb} from '#/platform/detection'
 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
 import {useOnboardingDispatch} from '#/state/shell'
-import {ScrollView} from '#/view/com/util/Views'
 import {Logo} from '#/view/icons/Logo'
 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index b0d71b929..b42b753e3 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Keyboard, TouchableOpacity, View} from 'react-native'
+import {Keyboard, View} from 'react-native'
 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {Image} from 'expo-image'
@@ -10,13 +10,12 @@ import {
   ModerationOpts,
 } from '@atproto/api'
 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
+import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
@@ -29,7 +28,7 @@ import {
   parseStarterPackUri,
 } from '#/lib/strings/starter-pack'
 import {logger} from '#/logger'
-import {isAndroid, isNative, isWeb} from '#/platform/detection'
+import {isNative} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useAllListMembersQuery} from '#/state/queries/list-members'
 import {useProfileQuery} from '#/state/queries/profile'
@@ -147,7 +146,6 @@ function WizardInner({
 }) {
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
-  const t = useTheme()
   const setMinimalShellMode = useSetMinimalShellMode()
   const [state, dispatch] = useWizardState()
   const {currentAccount} = useSession()
@@ -283,45 +281,24 @@ function WizardInner({
 
   return (
     <CenteredView style={[a.flex_1]} sideBorders>
-      <View
-        style={[
-          a.flex_row,
-          a.pb_sm,
-          a.px_md,
-          a.border_b,
-          t.atoms.border_contrast_medium,
-          a.gap_sm,
-          a.justify_between,
-          a.align_center,
-          isAndroid && a.pt_sm,
-          isWeb && [a.py_md],
-        ]}>
-        <View style={[{width: 65}]}>
-          <TouchableOpacity
-            testID="viewHeaderDrawerBtn"
-            hitSlop={HITSLOP_10}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Back`)}
-            accessibilityHint={_(msg`Go back to the previous step`)}
-            onPress={() => {
-              if (state.currentStep === 'Details') {
-                navigation.pop()
-              } else {
-                dispatch({type: 'Back'})
-              }
-            }}>
-            <FontAwesomeIcon
-              size={18}
-              icon="angle-left"
-              color={t.atoms.text.color}
-            />
-          </TouchableOpacity>
-        </View>
-        <Text style={[a.flex_1, a.font_bold, a.text_lg, a.text_center]}>
-          {currUiStrings.header}
-        </Text>
-        <View style={[{width: 65}]} />
-      </View>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton
+          label={_(msg`Back`)}
+          accessibilityHint={_(msg`Go back to the previous step`)}
+          onPress={evt => {
+            if (state.currentStep !== 'Details') {
+              evt.preventDefault()
+              dispatch({type: 'Back'})
+            }
+          }}
+        />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            {currUiStrings.header}
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
 
       <Container>
         {state.currentStep === 'Details' ? (
@@ -463,17 +440,17 @@ function Footer({
                 <Trans>
                   <Text style={[a.font_bold, textStyles]}>You</Text> and
                   <Text> </Text>
-                  <Text style={[a.font_bold, textStyles]}>
+                  <Text style={[a.font_bold, textStyles]} emoji>
                     {getName(items[1] /* [0] is self, skip it */)}{' '}
                   </Text>
                   are included in your starter pack
                 </Trans>
               ) : items.length > 2 ? (
                 <Trans context="profiles">
-                  <Text style={[a.font_bold, textStyles]}>
+                  <Text style={[a.font_bold, textStyles]} emoji>
                     {getName(items[1] /* [0] is self, skip it */)},{' '}
                   </Text>
-                  <Text style={[a.font_bold, textStyles]}>
+                  <Text style={[a.font_bold, textStyles]} emoji>
                     {getName(items[2])},{' '}
                   </Text>
                   and{' '}
@@ -504,29 +481,29 @@ function Footer({
               {
                 items.length === 1 ? (
                   <Trans>
-                    <Text style={[a.font_bold, textStyles]}>
+                    <Text style={[a.font_bold, textStyles]} emoji>
                       {getName(items[0])}
                     </Text>{' '}
                     is included in your starter pack
                   </Trans>
                 ) : items.length === 2 ? (
                   <Trans>
-                    <Text style={[a.font_bold, textStyles]}>
+                    <Text style={[a.font_bold, textStyles]} emoji>
                       {getName(items[0])}
                     </Text>{' '}
                     and
                     <Text> </Text>
-                    <Text style={[a.font_bold, textStyles]}>
+                    <Text style={[a.font_bold, textStyles]} emoji>
                       {getName(items[1])}{' '}
                     </Text>
                     are included in your starter pack
                   </Trans>
                 ) : items.length > 2 ? (
                   <Trans context="feeds">
-                    <Text style={[a.font_bold, textStyles]}>
+                    <Text style={[a.font_bold, textStyles]} emoji>
                       {getName(items[0])},{' '}
                     </Text>
-                    <Text style={[a.font_bold, textStyles]}>
+                    <Text style={[a.font_bold, textStyles]} emoji>
                       {getName(items[1])},{' '}
                     </Text>
                     and{' '}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 44e90a551..fa5a620bf 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -108,7 +108,7 @@ export function FeedPage({
   }, [scrollToTop, feed, queryClient, setHasNew])
 
   return (
-    <View testID={testID} style={s.h100pct}>
+    <View testID={testID}>
       <MainScrollProvider>
         <FeedFeedbackProvider value={feedFeedback}>
           <Feed
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index bdfc2c7ff..1dc67b6c3 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,26 +1,27 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {View} from 'react-native'
 import Animated from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {useKawaiiMode} from '#/state/preferences/kawaii'
 import {useSession} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
 import {Logo} from '#/view/icons/Logo'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
+import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
-import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
 
 export function HomeHeaderLayout(props: {
   children: React.ReactNode
   tabBarAnchor: JSX.Element | null | undefined
 }) {
-  const {isMobile} = useWebMediaQueries()
-  if (isMobile) {
+  const {gtMobile} = useBreakpoints()
+  if (!gtMobile) {
     return <HomeHeaderLayoutMobile {...props} />
   } else {
     return <HomeHeaderLayoutDesktopAndTablet {...props} />
@@ -40,98 +41,43 @@ function HomeHeaderLayoutDesktopAndTablet({
   const {hasSession} = useSession()
   const {_} = useLingui()
   const kawaii = useKawaiiMode()
+  const gutter = useGutterStyles()
 
   return (
     <>
       {hasSession && (
-        <View
-          style={[
-            a.relative,
-            a.flex_row,
-            a.justify_end,
-            a.align_center,
-            a.pt_lg,
-            a.px_md,
-            a.pb_2xs,
-            t.atoms.bg,
-            t.atoms.border_contrast_low,
-            styles.bar,
-            kawaii && {paddingTop: 22, paddingBottom: 16},
-          ]}>
+        <Layout.Center>
           <View
-            style={[
-              a.absolute,
-              a.inset_0,
-              a.pt_lg,
-              a.m_auto,
-              kawaii && {paddingTop: 4, paddingBottom: 0},
-              {
-                width: kawaii ? 84 : 28,
-              },
-            ]}>
-            <Logo width={kawaii ? 60 : 28} />
+            style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}>
+            <View style={{width: 34}} />
+            <View style={[a.flex_1, a.align_center, a.justify_center]}>
+              <Logo width={kawaii ? 60 : 28} />
+            </View>
+            <Link
+              to="/feeds"
+              hitSlop={10}
+              label={_(msg`View your feeds and explore more`)}
+              size="small"
+              variant="ghost"
+              color="secondary"
+              shape="square"
+              style={[a.justify_center]}>
+              <ButtonIcon icon={FeedsIcon} size="lg" />
+            </Link>
           </View>
-
-          <Link
-            to="/feeds"
-            hitSlop={10}
-            label={_(msg`View your feeds and explore more`)}
-            size="small"
-            variant="ghost"
-            color="secondary"
-            shape="square"
-            style={[
-              a.justify_center,
-              {
-                marginTop: -4,
-              },
-            ]}>
-            <FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} />
-          </Link>
-        </View>
+        </Layout.Center>
       )}
       {tabBarAnchor}
-      <Animated.View
-        onLayout={e => {
-          headerHeight.set(e.nativeEvent.layout.height)
-        }}
-        style={[
-          t.atoms.bg,
-          t.atoms.border_contrast_low,
-          styles.bar,
-          styles.tabBar,
-          headerMinimalShellTransform,
-        ]}>
-        {children}
-      </Animated.View>
+      <Layout.Center
+        style={[a.sticky, a.z_10, a.align_center, t.atoms.bg, {top: 0}]}>
+        <Animated.View
+          onLayout={e => {
+            headerHeight.set(e.nativeEvent.layout.height)
+          }}
+          style={[headerMinimalShellTransform]}>
+          {children}
+        </Animated.View>
+      </Layout.Center>
     </>
   )
 }
-
-const styles = StyleSheet.create({
-  bar: {
-    // @ts-ignore Web only
-    left: 'calc(50% - 300px)',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  topBar: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingHorizontal: 18,
-    paddingTop: 16,
-    paddingBottom: 8,
-  },
-  tabBar: {
-    // @ts-ignore Web only
-    position: 'sticky',
-    top: 0,
-    flexDirection: 'column',
-    alignItems: 'center',
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-    zIndex: 1,
-  },
-})
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index 832396092..e48c2cc89 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -1,25 +1,22 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {View} from 'react-native'
 import Animated from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {HITSLOP_10} from '#/lib/constants'
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {useHaptics} from '#/lib/haptics'
 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {isWeb} from '#/platform/detection'
+import {emitSoftReset} from '#/state/events'
 import {useSession} from '#/state/session'
-import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {Logo} from '#/view/icons/Logo'
-import {atoms} from '#/alf'
-import {useTheme} from '#/alf'
-import {atoms as a} from '#/alf'
-import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
+import {atoms as a, useTheme} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
-import {IS_DEV} from '#/env'
 
 export function HomeHeaderLayoutMobile({
   children,
@@ -28,58 +25,50 @@ export function HomeHeaderLayoutMobile({
   tabBarAnchor: JSX.Element | null | undefined
 }) {
   const t = useTheme()
-  const pal = usePalette('default')
   const {_} = useLingui()
-  const setDrawerOpen = useSetDrawerOpen()
   const {headerHeight} = useShellLayout()
   const headerMinimalShellTransform = useMinimalShellHeaderTransform()
   const {hasSession} = useSession()
-
-  const onPressAvi = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
+  const playHaptic = useHaptics()
 
   return (
     <Animated.View
-      style={[pal.border, styles.tabBar, headerMinimalShellTransform]}
+      style={[
+        a.fixed,
+        a.z_10,
+        t.atoms.bg,
+        {
+          top: 0,
+          left: 0,
+          right: 0,
+        },
+        headerMinimalShellTransform,
+      ]}
       onLayout={e => {
         headerHeight.set(e.nativeEvent.layout.height)
       }}>
-      <View style={[pal.view, styles.topBar]}>
-        <View style={[{width: 100}]}>
-          <TouchableOpacity
-            testID="viewHeaderDrawerBtn"
-            onPress={onPressAvi}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Open navigation`)}
-            accessibilityHint={_(
-              msg`Access profile and other navigation links`,
-            )}
-            hitSlop={HITSLOP_10}>
-            <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
-          </TouchableOpacity>
-        </View>
-        <View>
-          <Logo width={30} />
+      <Layout.Header.Outer noBottomBorder>
+        <Layout.Header.Slot>
+          <Layout.Header.MenuButton />
+        </Layout.Header.Slot>
+
+        <View style={[a.flex_1, a.align_center]}>
+          <PressableScale
+            targetScale={0.9}
+            onPress={() => {
+              emitSoftReset()
+            }}
+            onPressIn={() => {
+              playHaptic('Heavy')
+            }}
+            onPressOut={() => {
+              playHaptic('Light')
+            }}>
+            <Logo width={30} />
+          </PressableScale>
         </View>
-        <View
-          style={[
-            atoms.flex_row,
-            atoms.justify_end,
-            atoms.align_center,
-            atoms.gap_md,
-            {width: 100},
-          ]}>
-          {IS_DEV && (
-            <>
-              <Link
-                label="View storybook"
-                to="/sys/debug"
-                testID="storybookBtn">
-                <ColorPalette size="md" />
-              </Link>
-            </>
-          )}
+
+        <Layout.Header.Slot>
           {hasSession && (
             <Link
               testID="viewHeaderHomeFeedPrefsBtn"
@@ -93,40 +82,15 @@ export function HomeHeaderLayoutMobile({
               style={[
                 a.justify_center,
                 {
-                  marginTop: 2,
-                  marginRight: -6,
+                  marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET,
                 },
               ]}>
-              <FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
+              <ButtonIcon icon={FeedsIcon} size="lg" />
             </Link>
           )}
-        </View>
-      </View>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
       {children}
     </Animated.View>
   )
 }
-
-const styles = StyleSheet.create({
-  tabBar: {
-    // @ts-ignore web-only
-    position: isWeb ? 'fixed' : 'absolute',
-    zIndex: 1,
-    left: 0,
-    right: 0,
-    top: 0,
-    flexDirection: 'column',
-  },
-  topBar: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingHorizontal: 16,
-    paddingVertical: 5,
-    width: '100%',
-    minHeight: 46,
-  },
-  title: {
-    fontSize: 21,
-  },
-})
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index f9b147b29..f6b6223ce 100644
--- a/src/view/com/lightbox/Lightbox.web.tsx
+++ b/src/view/com/lightbox/Lightbox.web.tsx
@@ -15,8 +15,8 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {colors, s} from '#/lib/styles'
 import {useLightbox, useLightboxControls} from '#/state/lightbox'
@@ -28,7 +28,6 @@ export function Lightbox() {
   const {activeLightbox} = useLightbox()
   const {closeLightbox} = useLightboxControls()
   const isActive = !!activeLightbox
-  useWebBodyScrollLock(isActive)
 
   if (!isActive) {
     return null
@@ -37,11 +36,14 @@ export function Lightbox() {
   const initialIndex = activeLightbox.index
   const imgs = activeLightbox.images
   return (
-    <LightboxInner
-      imgs={imgs}
-      initialIndex={initialIndex}
-      onClose={closeLightbox}
-    />
+    <>
+      <RemoveScrollBar />
+      <LightboxInner
+        imgs={imgs}
+        initialIndex={initialIndex}
+        onClose={closeLightbox}
+      />
+    </>
   )
 }
 
diff --git a/src/view/com/lists/MyLists.tsx b/src/view/com/lists/MyLists.tsx
index 363dd100d..17327fd9a 100644
--- a/src/view/com/lists/MyLists.tsx
+++ b/src/view/com/lists/MyLists.tsx
@@ -15,7 +15,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
 import {cleanError} from '#/lib/strings/errors'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
 import {EmptyState} from '#/view/com/util/EmptyState'
@@ -110,7 +109,7 @@ export function MyLists({
       ) : (
         <View
           style={[
-            (index !== 0 || isWeb) && a.border_t,
+            index !== 0 && a.border_t,
             t.atoms.border_contrast_low,
             a.px_lg,
             a.py_lg,
@@ -141,8 +140,6 @@ export function MyLists({
             }
             contentContainerStyle={[s.contentContainer]}
             removeClippedSubviews={true}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
           />
         )}
       </View>
@@ -160,8 +157,8 @@ export function MyLists({
             onRefresh={onRefresh}
             contentContainerStyle={[s.contentContainer]}
             removeClippedSubviews={true}
-            // @ts-ignore our .web version only -prf
             desktopFixedHeight
+            sideBorders={false}
           />
         )}
       </View>
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 8d93c21b4..0c49c8771 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -1,8 +1,8 @@
 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import type {Modal as ModalIface} from '#/state/modals'
 import {useModalControls, useModals} from '#/state/modals'
@@ -22,7 +22,6 @@ import * as VerifyEmailModal from './VerifyEmail'
 
 export function ModalsContainer() {
   const {isModalActive, activeModals} = useModals()
-  useWebBodyScrollLock(isModalActive)
 
   if (!isModalActive) {
     return null
@@ -30,6 +29,7 @@ export function ModalsContainer() {
 
   return (
     <>
+      <RemoveScrollBar />
       {activeModals.map((modal, i) => (
         <Modal key={`modal-${i}`} modal={modal} />
       ))}
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index bd39ddd84..9871455a1 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
 
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {cleanError} from '#/lib/strings/errors'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
@@ -22,7 +21,6 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {List, ListRef} from '#/view/com/util/List'
 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
-import {CenteredView} from '#/view/com/util/Views'
 import {FeedItem} from './FeedItem'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -46,7 +44,6 @@ export function Feed({
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const pal = usePalette('default')
-  const {isTabletOrMobile} = useWebMediaQueries()
 
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
@@ -133,11 +130,7 @@ export function Feed({
         )
       } else if (item === LOADING_ITEM) {
         return (
-          <View
-            style={[
-              pal.border,
-              !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth},
-            ]}>
+          <View style={[pal.border]}>
             <NotificationFeedLoadingPlaceholder />
           </View>
         )
@@ -146,11 +139,11 @@ export function Feed({
         <FeedItem
           item={item}
           moderationOpts={moderationOpts!}
-          hideTopBorder={index === 0 && isTabletOrMobile}
+          hideTopBorder={index === 0}
         />
       )
     },
-    [moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border],
+    [moderationOpts, _, onPressRetryLoadMore, pal.border],
   )
 
   const FeedFooter = React.useCallback(
@@ -168,12 +161,10 @@ export function Feed({
   return (
     <View style={s.hContentRegion}>
       {error && (
-        <CenteredView>
-          <ErrorMessage
-            message={cleanError(error)}
-            onPressTryAgain={onPressTryAgain}
-          />
-        </CenteredView>
+        <ErrorMessage
+          message={cleanError(error)}
+          onPressTryAgain={onPressTryAgain}
+        />
       )}
       <List
         testID="notifsFeed"
diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx
index 13c723f47..3335532b3 100644
--- a/src/view/com/pager/PagerWithHeader.web.tsx
+++ b/src/view/com/pager/PagerWithHeader.web.tsx
@@ -1,10 +1,10 @@
 import * as React from 'react'
-import {ScrollView, StyleSheet, View} from 'react-native'
+import {ScrollView, View} from 'react-native'
 import {useAnimatedRef} from 'react-native-reanimated'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {atoms as a, web} from '#/alf'
+import * as Layout from '#/components/Layout'
 import {ListMethods} from '../util/List'
 import {TabBar} from './TabBar'
 
@@ -121,30 +121,19 @@ let PagerTabBar = ({
   onSelect?: (index: number) => void
   tabBarAnchor?: JSX.Element | null | undefined
 }): React.ReactNode => {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
   return (
     <>
-      <View
-        style={[
-          !isMobile && styles.headerContainerDesktop,
-          pal.border,
-          !isHeaderReady && styles.loadingHeader,
-        ]}>
-        {renderHeader?.()}
-      </View>
+      <Layout.Center>{renderHeader?.()}</Layout.Center>
       {tabBarAnchor}
-      <View
-        style={[
-          styles.tabBarContainer,
-          isMobile
-            ? styles.tabBarContainerMobile
-            : styles.tabBarContainerDesktop,
-          pal.border,
+      <Layout.Center
+        style={web([
+          a.sticky,
+          a.z_10,
           {
+            top: 0,
             display: isHeaderReady ? undefined : 'none',
           },
-        ]}>
+        ])}>
         <TabBar
           testID={testID}
           items={items}
@@ -154,7 +143,7 @@ let PagerTabBar = ({
           dragProgress={undefined as any /* native-only */}
           dragState={undefined as any /* native-only */}
         />
-      </View>
+      </Layout.Center>
     </>
   )
 }
@@ -180,33 +169,6 @@ function PagerItem({
   })
 }
 
-const styles = StyleSheet.create({
-  headerContainerDesktop: {
-    marginHorizontal: 'auto',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  tabBarContainer: {
-    // @ts-ignore web-only
-    position: 'sticky',
-    top: 0,
-    zIndex: 1,
-  },
-  tabBarContainerDesktop: {
-    marginHorizontal: 'auto',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  tabBarContainerMobile: {
-    paddingHorizontal: 0,
-  },
-  loadingHeader: {
-    borderColor: 'transparent',
-  },
-})
-
 function toArray<T>(v: T | T[]): T[] {
   if (Array.isArray(v)) {
     return v
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 4c0d973a9..b9051a9c6 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useLikedByQuery} from '#/state/queries/post-liked-by'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
@@ -18,7 +17,7 @@ function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
     <ProfileCardWithFollowBtn
       key={item.actor.did}
       profile={item.actor}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
@@ -88,6 +87,7 @@ export function PostLikedBy({uri}: {uri: string}) {
         )}
         errorMessage={cleanError(resolveError || error)}
         sideBorders={false}
+        topBorder={false}
       />
     )
   }
@@ -108,7 +108,6 @@ export function PostLikedBy({uri}: {uri: string}) {
           onRetry={fetchNextPage}
         />
       }
-      // @ts-ignore our .web version only -prf
       desktopFixedHeight
       initialNumToRender={initialNumToRender}
       windowSize={11}
diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx
index 10a51166c..a22000b96 100644
--- a/src/view/com/post-thread/PostQuotes.tsx
+++ b/src/view/com/post-thread/PostQuotes.tsx
@@ -11,7 +11,6 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePostQuotesQuery} from '#/state/queries/post-quotes'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
@@ -30,7 +29,7 @@ function renderItem({
   }
   index: number
 }) {
-  return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} />
+  return <Post post={item.post} hideTopBorder={index === 0} />
 }
 
 function keyExtractor(item: {
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index dfaa69780..2143bd9c2 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -12,8 +12,20 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {List} from '#/view/com/util/List'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
-function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
-  return <ProfileCardWithFollowBtn key={item.did} profile={item} />
+function renderItem({
+  item,
+  index,
+}: {
+  item: ActorDefs.ProfileViewBasic
+  index: number
+}) {
+  return (
+    <ProfileCardWithFollowBtn
+      key={item.did}
+      profile={item}
+      noBorder={index === 0}
+    />
+  )
 }
 
 function keyExtractor(item: ActorDefs.ProfileViewBasic) {
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index a10149395..0cdccff59 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -32,7 +32,6 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {CenteredView} from '#/view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 import {Text} from '#/components/Typography'
@@ -484,7 +483,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
   }
 
   return (
-    <CenteredView style={[a.flex_1]} sideBorders={true}>
+    <>
       {showHeader && (
         <ViewHeader
           title={_(msg({message: `Post`, context: 'description'}))}
@@ -531,7 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
       {isMobile && canReply && hasSession && (
         <MobileComposePrompt onPressReply={onPressReply} />
       )}
-    </CenteredView>
+    </>
   )
 }
 
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 60a7a5e31..3c0476929 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useSession} from '#/state/session'
@@ -25,7 +24,7 @@ function renderItem({
     <ProfileCardWithFollowBtn
       key={item.did}
       profile={item}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 572b0b9f4..1cd65c74c 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useSession} from '#/state/session'
@@ -25,7 +24,7 @@ function renderItem({
     <ProfileCardWithFollowBtn
       key={item.did}
       profile={item}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index 0e25fe5e6..cd11611a8 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -1,29 +1,24 @@
 import React from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
+import {Pressable, View} from 'react-native'
 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-import {BACK_HITSLOP} from '#/lib/constants'
 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {makeProfileLink} from '#/lib/routes/links'
 import {NavigationProp} from '#/lib/routes/types'
 import {sanitizeHandle} from '#/lib/strings/handles'
-import {isNative} from '#/platform/detection'
 import {emitSoftReset} from '#/state/events'
 import {useLightboxControls} from '#/state/lightbox'
-import {useSetDrawerOpen} from '#/state/shell'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {TextLink} from '#/view/com/util/Link'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {Text} from '#/view/com/util/text/Text'
+import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
 import {StarterPack} from '#/components/icons/StarterPack'
-import {TextLink} from '../util/Link'
-import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {Text} from '../util/text/Text'
-import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
-import {CenteredView} from '../util/Views'
+import * as Layout from '#/components/Layout'
 
 export function ProfileSubpageHeader({
   isLoading,
@@ -48,7 +43,6 @@ export function ProfileSubpageHeader({
     | undefined
   avatarType: UserAvatarType | 'starter-pack'
 }>) {
-  const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
@@ -57,18 +51,6 @@ export function ProfileSubpageHeader({
   const canGoBack = navigation.canGoBack()
   const aviRef = useHandleRef()
 
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
   const _openLightbox = React.useCallback(
     (uri: string, thumbRect: MeasuredDimensions | null) => {
       openLightbox({
@@ -106,42 +88,17 @@ export function ProfileSubpageHeader({
   }, [_openLightbox, avatar, aviRef])
 
   return (
-    <CenteredView style={pal.view}>
-      {isMobile && (
-        <View
-          style={[
-            {
-              flexDirection: 'row',
-              alignItems: 'center',
-              borderBottomWidth: StyleSheet.hairlineWidth,
-              paddingTop: isNative ? 0 : 8,
-              paddingBottom: 8,
-              paddingHorizontal: isMobile ? 12 : 14,
-            },
-            pal.border,
-          ]}>
-          <Pressable
-            testID="headerDrawerBtn"
-            onPress={canGoBack ? onPressBack : onPressMenu}
-            hitSlop={BACK_HITSLOP}
-            style={canGoBack ? styles.backBtn : styles.backBtnWide}
-            accessibilityRole="button"
-            accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
-            accessibilityHint="">
-            {canGoBack ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="angle-left"
-                style={[styles.backIcon, pal.text]}
-              />
-            ) : (
-              <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
-            )}
-          </Pressable>
-          <View style={{flex: 1}} />
-          {children}
-        </View>
-      )}
+    <>
+      <Layout.Header.Outer>
+        {canGoBack ? (
+          <Layout.Header.BackButton />
+        ) : (
+          <Layout.Header.MenuButton />
+        )}
+        <Layout.Header.Content />
+        {children}
+      </Layout.Header.Outer>
+
       <View
         style={{
           flexDirection: 'row',
@@ -206,31 +163,7 @@ export function ProfileSubpageHeader({
             </Text>
           )}
         </View>
-        {!isMobile && (
-          <View
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-            }}>
-            {children}
-          </View>
-        )}
       </View>
-    </CenteredView>
+    </>
   )
 }
-
-const styles = StyleSheet.create({
-  backBtn: {
-    width: 20,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 20,
-    height: 30,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx
index f112d2d0a..18f7d6fa7 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -4,10 +4,9 @@ import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook
 
 import {batchedUpdates} from '#/lib/batchedUpdates'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {useScrollHandlers} from '#/lib/ScrollContext'
 import {addStyle} from '#/lib/styles'
+import * as Layout from '#/components/Layout'
 
 export type ListMethods = any // TODO: Better types.
 export type ListProps<ItemT> = Omit<
@@ -24,6 +23,9 @@ export type ListProps<ItemT> = Omit<
   desktopFixedHeight?: number | boolean
   // Web only prop to contain the scroll to the container rather than the window
   disableFullWindowScroll?: boolean
+  /**
+   * @deprecated Should be using Layout components
+   */
   sideBorders?: boolean
 }
 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
@@ -56,20 +58,11 @@ function ListImpl<ItemT>(
     renderItem,
     extraData,
     style,
-    sideBorders = true,
     ...props
   }: ListProps<ItemT>,
   ref: React.Ref<ListMethods>,
 ) {
   const contextScrollHandlers = useScrollHandlers()
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  if (!isMobile) {
-    contentContainerStyle = addStyle(
-      contentContainerStyle,
-      styles.containerScroll,
-    )
-  }
 
   const isEmpty = !data || data.length === 0
 
@@ -326,53 +319,53 @@ function ListImpl<ItemT>(
           styles.parentTreeVisibilityDetector
         }
       />
-      <View
-        ref={containerRef}
-        style={[
-          !isMobile && sideBorders && styles.sideBorders,
-          contentContainerStyle,
-          desktopFixedHeight ? styles.minHeightViewport : null,
-          pal.border,
-        ]}>
-        <Visibility
-          root={disableFullWindowScroll ? nativeRef : null}
-          onVisibleChange={handleAboveTheFoldVisibleChange}
-          style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
-        />
-        {onStartReached && !isEmpty && (
-          <EdgeVisibility
-            root={disableFullWindowScroll ? nativeRef : null}
-            onVisibleChange={onHeadVisibilityChange}
-            topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
-            containerRef={containerRef}
-          />
-        )}
-        {headerComponent}
-        {isEmpty
-          ? emptyComponent
-          : (data as Array<ItemT>)?.map((item, index) => {
-              const key = keyExtractor!(item, index)
-              return (
-                <Row<ItemT>
-                  key={key}
-                  item={item}
-                  index={index}
-                  renderItem={renderItem}
-                  extraData={extraData}
-                  onItemSeen={onItemSeen}
-                />
-              )
-            })}
-        {onEndReached && !isEmpty && (
-          <EdgeVisibility
+      <Layout.Center>
+        <View
+          ref={containerRef}
+          style={[
+            contentContainerStyle,
+            desktopFixedHeight ? styles.minHeightViewport : null,
+          ]}>
+          <Visibility
             root={disableFullWindowScroll ? nativeRef : null}
-            onVisibleChange={onTailVisibilityChange}
-            bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
-            containerRef={containerRef}
+            onVisibleChange={handleAboveTheFoldVisibleChange}
+            style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
           />
-        )}
-        {footerComponent}
-      </View>
+          {onStartReached && !isEmpty && (
+            <EdgeVisibility
+              root={disableFullWindowScroll ? nativeRef : null}
+              onVisibleChange={onHeadVisibilityChange}
+              topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
+              containerRef={containerRef}
+            />
+          )}
+          {headerComponent}
+          {isEmpty
+            ? emptyComponent
+            : (data as Array<ItemT>)?.map((item, index) => {
+                const key = keyExtractor!(item, index)
+                return (
+                  <Row<ItemT>
+                    key={key}
+                    item={item}
+                    index={index}
+                    renderItem={renderItem}
+                    extraData={extraData}
+                    onItemSeen={onItemSeen}
+                  />
+                )
+              })}
+          {onEndReached && !isEmpty && (
+            <EdgeVisibility
+              root={disableFullWindowScroll ? nativeRef : null}
+              onVisibleChange={onTailVisibilityChange}
+              bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
+              containerRef={containerRef}
+            />
+          )}
+          {footerComponent}
+        </View>
+      </Layout.Center>
     </View>
   )
 }
@@ -558,16 +551,6 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
 // https://stackoverflow.com/questions/7944460/detect-safari-browser
 
 const styles = StyleSheet.create({
-  sideBorders: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  containerScroll: {
-    width: '100%',
-    maxWidth: 600,
-    marginLeft: 'auto',
-    marginRight: 'auto',
-  },
   minHeightViewport: {
     // @ts-ignore web only
     minHeight: '100vh',
diff --git a/src/view/com/util/LoadingScreen.tsx b/src/view/com/util/LoadingScreen.tsx
index 5d2aeb38f..1086c9d17 100644
--- a/src/view/com/util/LoadingScreen.tsx
+++ b/src/view/com/util/LoadingScreen.tsx
@@ -1,14 +1,17 @@
 import {ActivityIndicator, View} from 'react-native'
 
 import {s} from '#/lib/styles'
-import {CenteredView} from './Views'
+import * as Layout from '#/components/Layout'
 
+/**
+ * @deprecated use Layout compoenents directly
+ */
 export function LoadingScreen() {
   return (
-    <CenteredView>
+    <Layout.Content>
       <View style={s.p20}>
         <ActivityIndicator size="large" />
       </View>
-    </CenteredView>
+    </Layout.Content>
   )
 }
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
deleted file mode 100644
index 78b66a929..000000000
--- a/src/view/com/util/SimpleViewHeader.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react'
-import {
-  StyleProp,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useNavigation} from '@react-navigation/native'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {NavigationProp} from '#/lib/routes/types'
-import {isWeb} from '#/platform/detection'
-import {useSetDrawerOpen} from '#/state/shell'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {CenteredView} from './Views'
-
-const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
-
-export function SimpleViewHeader({
-  showBackButton = true,
-  style,
-  children,
-}: React.PropsWithChildren<{
-  showBackButton?: boolean
-  style?: StyleProp<ViewStyle>
-}>) {
-  const pal = usePalette('default')
-  const setDrawerOpen = useSetDrawerOpen()
-  const navigation = useNavigation<NavigationProp>()
-  const {isMobile} = useWebMediaQueries()
-  const canGoBack = navigation.canGoBack()
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
-  const Container = isMobile ? View : CenteredView
-  return (
-    <Container
-      style={[
-        styles.header,
-        isMobile && styles.headerMobile,
-        isWeb && styles.headerWeb,
-        pal.view,
-        style,
-      ]}>
-      {showBackButton ? (
-        <TouchableOpacity
-          testID="viewHeaderDrawerBtn"
-          onPress={canGoBack ? onPressBack : onPressMenu}
-          hitSlop={BACK_HITSLOP}
-          style={canGoBack ? styles.backBtn : styles.backBtnWide}
-          accessibilityRole="button"
-          accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
-          accessibilityHint="">
-          {canGoBack ? (
-            <FontAwesomeIcon
-              size={18}
-              icon="angle-left"
-              style={[styles.backIcon, pal.text]}
-            />
-          ) : (
-            <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
-          )}
-        </TouchableOpacity>
-      ) : null}
-      {children}
-    </Container>
-  )
-}
-
-const styles = StyleSheet.create({
-  header: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingHorizontal: 18,
-    paddingVertical: 12,
-    width: '100%',
-  },
-  headerMobile: {
-    paddingHorizontal: 12,
-    paddingVertical: 10,
-  },
-  headerWeb: {
-    // @ts-ignore web-only
-    position: 'sticky',
-    top: 0,
-    zIndex: 1,
-  },
-  backBtn: {
-    width: 30,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 30,
-    height: 30,
-    paddingLeft: 4,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 1d4cf8ff0..2d413f782 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -1,271 +1,27 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import Animated from 'react-native-reanimated'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
-
-import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {NavigationProp} from '#/lib/routes/types'
-import {useSetDrawerOpen} from '#/state/shell'
-import {useTheme} from '#/alf'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {Text} from './text/Text'
-import {CenteredView} from './Views'
-
-const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
+import {Header} from '#/components/Layout'
 
+/**
+ * Legacy ViewHeader component. Use Layout.Header going forward.
+ *
+ * @deprecated
+ */
 export function ViewHeader({
   title,
-  subtitle,
-  canGoBack,
-  showBackButton = true,
-  hideOnScroll,
-  showOnDesktop,
-  showBorder,
   renderButton,
 }: {
   title: string
   subtitle?: string
-  canGoBack?: boolean
-  showBackButton?: boolean
-  hideOnScroll?: boolean
   showOnDesktop?: boolean
   showBorder?: boolean
   renderButton?: () => JSX.Element
 }) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const setDrawerOpen = useSetDrawerOpen()
-  const navigation = useNavigation<NavigationProp>()
-  const {isDesktop, isTablet} = useWebMediaQueries()
-  const t = useTheme()
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
-  if (isDesktop) {
-    if (showOnDesktop) {
-      return (
-        <DesktopWebHeader
-          title={title}
-          subtitle={subtitle}
-          renderButton={renderButton}
-          showBorder={showBorder}
-        />
-      )
-    }
-    return null
-  } else {
-    if (typeof canGoBack === 'undefined') {
-      canGoBack = navigation.canGoBack()
-    }
-
-    return (
-      <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
-        <View style={{flex: 1}}>
-          <View style={{flexDirection: 'row', alignItems: 'center'}}>
-            {showBackButton ? (
-              <TouchableOpacity
-                testID="viewHeaderDrawerBtn"
-                onPress={canGoBack ? onPressBack : onPressMenu}
-                hitSlop={BACK_HITSLOP}
-                style={canGoBack ? styles.backBtn : styles.backBtnWide}
-                accessibilityRole="button"
-                accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
-                accessibilityHint={
-                  canGoBack ? '' : _(msg`Access navigation links and settings`)
-                }>
-                {canGoBack ? (
-                  <FontAwesomeIcon
-                    size={18}
-                    icon="angle-left"
-                    style={[styles.backIcon, pal.text]}
-                  />
-                ) : !isTablet ? (
-                  <Menu size="lg" style={[{marginTop: 3}, pal.textLight]} />
-                ) : null}
-              </TouchableOpacity>
-            ) : null}
-            <View style={styles.titleContainer} pointerEvents="none">
-              <Text emoji type="title" style={[pal.text, styles.title]}>
-                {title}
-              </Text>
-            </View>
-            {renderButton ? (
-              renderButton()
-            ) : showBackButton ? (
-              <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
-            ) : null}
-          </View>
-          {subtitle ? (
-            <View
-              style={[styles.titleContainer, {marginTop: -3}]}
-              pointerEvents="none">
-              <Text
-                style={[
-                  pal.text,
-                  styles.subtitle,
-                  t.atoms.text_contrast_medium,
-                ]}>
-                {subtitle}
-              </Text>
-            </View>
-          ) : undefined}
-        </View>
-      </Container>
-    )
-  }
-}
-
-function DesktopWebHeader({
-  title,
-  subtitle,
-  renderButton,
-  showBorder = true,
-}: {
-  title: string
-  subtitle?: string
-  renderButton?: () => JSX.Element
-  showBorder?: boolean
-}) {
-  const pal = usePalette('default')
-  const t = useTheme()
-  return (
-    <CenteredView
-      style={[
-        styles.header,
-        styles.desktopHeader,
-        pal.border,
-        {
-          borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0,
-        },
-        {display: 'flex', flexDirection: 'column'},
-      ]}>
-      <View>
-        <View style={styles.titleContainer} pointerEvents="none">
-          <Text type="title-lg" style={[pal.text, styles.title]}>
-            {title}
-          </Text>
-        </View>
-        {renderButton?.()}
-      </View>
-      {subtitle ? (
-        <View>
-          <View style={[styles.titleContainer]} pointerEvents="none">
-            <Text
-              style={[
-                pal.text,
-                styles.subtitleDesktop,
-                t.atoms.text_contrast_medium,
-              ]}>
-              {subtitle}
-            </Text>
-          </View>
-        </View>
-      ) : null}
-    </CenteredView>
-  )
-}
-
-function Container({
-  children,
-  hideOnScroll,
-  showBorder,
-}: {
-  children: React.ReactNode
-  hideOnScroll: boolean
-  showBorder?: boolean
-}) {
-  const pal = usePalette('default')
-  const headerMinimalShellTransform = useMinimalShellHeaderTransform()
-
-  if (!hideOnScroll) {
-    return (
-      <View
-        style={[
-          styles.header,
-          pal.view,
-          pal.border,
-          showBorder && styles.border,
-        ]}>
-        {children}
-      </View>
-    )
-  }
   return (
-    <Animated.View
-      style={[
-        styles.header,
-        styles.headerFloating,
-        pal.view,
-        pal.border,
-        headerMinimalShellTransform,
-        showBorder && styles.border,
-      ]}>
-      {children}
-    </Animated.View>
+    <Header.Outer>
+      <Header.BackButton />
+      <Header.Content>
+        <Header.TitleText>{title}</Header.TitleText>
+      </Header.Content>
+      <Header.Slot>{renderButton?.() ?? null}</Header.Slot>
+    </Header.Outer>
   )
 }
-
-const styles = StyleSheet.create({
-  header: {
-    flexDirection: 'row',
-    paddingHorizontal: 12,
-    paddingVertical: 6,
-    width: '100%',
-  },
-  headerFloating: {
-    position: 'absolute',
-    top: 0,
-    width: '100%',
-  },
-  desktopHeader: {
-    paddingVertical: 12,
-    maxWidth: 600,
-    marginLeft: 'auto',
-    marginRight: 'auto',
-  },
-  border: {
-    borderBottomWidth: StyleSheet.hairlineWidth,
-  },
-  titleContainer: {
-    marginLeft: 'auto',
-    marginRight: 'auto',
-    alignItems: 'center',
-  },
-  title: {
-    fontWeight: '600',
-  },
-  subtitle: {
-    fontSize: 13,
-  },
-  subtitleDesktop: {
-    fontSize: 15,
-  },
-  backBtn: {
-    width: 30,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 30,
-    height: 30,
-    paddingLeft: 4,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/Views.tsx b/src/view/com/util/Views.tsx
index 0d3f63794..c9ba0728c 100644
--- a/src/view/com/util/Views.tsx
+++ b/src/view/com/util/Views.tsx
@@ -15,9 +15,16 @@ export type FlatList_INTERNAL<ItemT = any> = Omit<
   FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>,
   'CellRendererComponent'
 >
+
+/**
+ * @deprecated use `Layout` components
+ */
 export const ScrollView = Animated.ScrollView
 export type ScrollView = typeof Animated.ScrollView
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const CenteredView = forwardRef<
   View,
   React.PropsWithChildren<
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 1f030b408..e64b0ce9a 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -31,10 +31,12 @@ interface AddedProps {
   desktopFixedHeight?: boolean | number
 }
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const CenteredView = React.forwardRef(function CenteredView(
   {
     style,
-    sideBorders,
     topBorder,
     ...props
   }: React.PropsWithChildren<
@@ -47,13 +49,6 @@ export const CenteredView = React.forwardRef(function CenteredView(
   if (!isMobile) {
     style = addStyle(style, styles.container)
   }
-  if (sideBorders && !isMobile) {
-    style = addStyle(style, {
-      borderLeftWidth: StyleSheet.hairlineWidth,
-      borderRightWidth: StyleSheet.hairlineWidth,
-    })
-    style = addStyle(style, pal.border)
-  }
   if (topBorder) {
     style = addStyle(style, {
       borderTopWidth: 1,
@@ -75,7 +70,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   >,
   ref: React.Ref<FlatList<ItemT>>,
 ) {
-  const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
     contentContainerStyle = addStyle(
@@ -123,11 +117,7 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   return (
     <Animated.FlatList
       ref={ref}
-      contentContainerStyle={[
-        styles.contentContainer,
-        contentContainerStyle,
-        pal.border,
-      ]}
+      contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
       style={style}
       contentOffset={contentOffset}
       {...props}
@@ -135,12 +125,13 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   )
 })
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const ScrollView = React.forwardRef(function ScrollViewImpl(
   {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
   ref: React.Ref<Animated.ScrollView>,
 ) {
-  const pal = usePalette('default')
-
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
     contentContainerStyle = addStyle(
@@ -150,11 +141,7 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
   }
   return (
     <Animated.ScrollView
-      contentContainerStyle={[
-        styles.contentContainer,
-        contentContainerStyle,
-        pal.border,
-      ]}
+      contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
       // @ts-ignore something is wrong with the reanimated types -prf
       ref={ref}
       {...props}
@@ -164,8 +151,6 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
 
 const styles = StyleSheet.create({
   contentContainer: {
-    borderLeftWidth: StyleSheet.hairlineWidth,
-    borderRightWidth: StyleSheet.hairlineWidth,
     // @ts-ignore web only
     minHeight: '100vh',
   },
diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index b66f43789..846f4d295 100644
--- a/src/view/com/util/error/ErrorScreen.tsx
+++ b/src/view/com/util/error/ErrorScreen.tsx
@@ -36,7 +36,9 @@ export function ErrorScreen({
 
   return (
     <>
-      {showHeader && isMobile && <ViewHeader title="Error" showBorder />}
+      {showHeader && isMobile && (
+        <ViewHeader title={_(msg`Error`)} showBorder />
+      )}
       <CenteredView testID={testID} style={[styles.outer, pal.view]}>
         <View style={styles.errorIconContainer}>
           <View
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 406f11792..0dcf1f016 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -24,14 +24,13 @@ import {useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {FAB} from '#/view/com/util/fab/FAB'
-import {TextLink} from '#/view/com/util/Link'
 import {List, ListMethods} from '#/view/com/util/List'
 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {Text} from '#/view/com/util/text/Text'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
 import {atoms as a, useTheme} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import * as FeedCard from '#/components/FeedCard'
 import {SearchInput} from '#/components/forms/SearchInput'
@@ -40,7 +39,9 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components
 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
 import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
+import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
 import * as Layout from '#/components/Layout'
+import {Link} from '#/components/Link'
 import * as ListCard from '#/components/ListCard'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
@@ -102,7 +103,7 @@ type FlatlistSlice =
 export function FeedsScreen(_props: Props) {
   const pal = usePalette('default')
   const {openComposer} = useComposerControls()
-  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+  const {isMobile} = useWebMediaQueries()
   const [query, setQuery] = React.useState('')
   const [isPTR, setIsPTR] = React.useState(false)
   const {
@@ -374,22 +375,6 @@ export function FeedsScreen(_props: Props) {
     isUserSearching,
   ])
 
-  const renderHeaderBtn = React.useCallback(() => {
-    return (
-      <View style={styles.headerBtnGroup}>
-        <TextLink
-          testID="editFeedsBtn"
-          type="lg-medium"
-          href="/settings/saved-feeds"
-          accessibilityLabel={_(msg`Edit My Feeds`)}
-          accessibilityHint=""
-          text={_(msg`Edit`)}
-          style={[pal.link, a.pr_xs]}
-        />
-      </View>
-    )
-  }, [pal, _])
-
   const searchBarIndex = items.findIndex(
     item => item.type === 'popularFeedsHeader',
   )
@@ -430,36 +415,7 @@ export function FeedsScreen(_props: Props) {
           </View>
         )
       } else if (item.type === 'savedFeedsHeader') {
-        return (
-          <>
-            {!isMobile && (
-              <View
-                style={[
-                  pal.view,
-                  styles.header,
-                  pal.border,
-                  {
-                    borderBottomWidth: 1,
-                  },
-                ]}>
-                <Text type="title-lg" style={[pal.text, s.bold]}>
-                  <Trans>Feeds</Trans>
-                </Text>
-                <View style={styles.headerBtnGroup}>
-                  <TextLink
-                    type="lg"
-                    href="/settings/saved-feeds"
-                    accessibilityLabel={_(msg`Edit My Feeds`)}
-                    accessibilityHint=""
-                    text={_(msg`Edit`)}
-                    style={[pal.link]}
-                  />
-                </View>
-              </View>
-            )}
-            <FeedsSavedHeader />
-          </>
-        )
+        return <FeedsSavedHeader />
       } else if (item.type === 'savedFeedNoResults') {
         return (
           <View
@@ -530,13 +486,8 @@ export function FeedsScreen(_props: Props) {
       return null
     },
     [
-      isMobile,
-      pal.view,
       pal.border,
-      pal.text,
       pal.textLight,
-      pal.link,
-      _,
       query,
       onChangeQuery,
       onPressCancelSearch,
@@ -547,31 +498,45 @@ export function FeedsScreen(_props: Props) {
 
   return (
     <Layout.Screen testID="FeedsScreen">
-      {isMobile && (
-        <ViewHeader
-          title={_(msg`Feeds`)}
-          renderButton={renderHeaderBtn}
-          showBorder
+      <Layout.Center>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Feeds</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot>
+            <Link
+              testID="editFeedsBtn"
+              to="/settings/saved-feeds"
+              label={_(msg`Edit My Feeds`)}
+              size="small"
+              variant="ghost"
+              color="secondary"
+              shape="round"
+              style={[a.justify_center, {right: -3}]}>
+              <ButtonIcon icon={Gear} size="lg" />
+            </Link>
+          </Layout.Header.Slot>
+        </Layout.Header.Outer>
+
+        <List
+          ref={listRef}
+          data={items}
+          keyExtractor={item => item.key}
+          contentContainerStyle={styles.contentContainer}
+          renderItem={renderItem}
+          refreshing={isPTR}
+          onRefresh={isUserSearching ? undefined : onPullToRefresh}
+          initialNumToRender={10}
+          onEndReached={onEndReached}
+          desktopFixedHeight
+          keyboardShouldPersistTaps="handled"
+          keyboardDismissMode="on-drag"
+          sideBorders={false}
         />
-      )}
-
-      <List
-        ref={listRef}
-        style={[!isTabletOrDesktop && s.flex1, styles.list]}
-        data={items}
-        keyExtractor={item => item.key}
-        contentContainerStyle={styles.contentContainer}
-        renderItem={renderItem}
-        refreshing={isPTR}
-        onRefresh={isUserSearching ? undefined : onPullToRefresh}
-        initialNumToRender={10}
-        onEndReached={onEndReached}
-        // @ts-ignore our .web version only -prf
-        desktopFixedHeight
-        scrollIndicatorInsets={{right: 1}}
-        keyboardShouldPersistTaps="handled"
-        keyboardDismissMode="on-drag"
-      />
+      </Layout.Center>
 
       {hasSession && (
         <FAB
@@ -728,7 +693,7 @@ function FeedsSavedHeader() {
       }>
       <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" />
       <View style={[a.flex_1, a.gap_xs]}>
-        <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
+        <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}>
           <Trans>My Feeds</Trans>
         </Text>
         <Text style={[t.atoms.text_contrast_high]}>
@@ -754,7 +719,7 @@ function FeedsAboutHeader() {
         size="lg"
       />
       <View style={[a.flex_1, a.gap_sm]}>
-        <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
+        <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}>
           <Trans>Discover New Feeds</Trans>
         </Text>
         <Text style={[t.atoms.text_contrast_high]}>
@@ -769,9 +734,6 @@ function FeedsAboutHeader() {
 }
 
 const styles = StyleSheet.create({
-  list: {
-    height: '100%',
-  },
   contentContainer: {
     paddingBottom: 100,
   },
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index f654f2bd9..99abf0603 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -1,33 +1,26 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
 import {AtUri} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 
 import {useEmail} from '#/lib/hooks/useEmail'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {NavigationProp} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
 import {useModalControls} from '#/state/modals'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {MyLists} from '#/view/com/lists/MyLists'
-import {Button} from '#/view/com/util/forms/Button'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {Text} from '#/view/com/util/text/Text'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {useDialogControl} from '#/components/Dialog'
 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
 export function ListsScreen({}: Props) {
   const {_} = useLingui()
-  const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isMobile} = useWebMediaQueries()
   const navigation = useNavigation<NavigationProp>()
   const {openModal} = useModalControls()
   const {needsEmailVerification} = useEmail()
@@ -62,43 +55,30 @@ export function ListsScreen({}: Props) {
 
   return (
     <Layout.Screen testID="listsScreen">
-      <SimpleViewHeader
-        showBackButton={isMobile}
-        style={[
-          pal.border,
-          isMobile
-            ? {borderBottomWidth: StyleSheet.hairlineWidth}
-            : {
-                borderLeftWidth: StyleSheet.hairlineWidth,
-                borderRightWidth: StyleSheet.hairlineWidth,
-              },
-        ]}>
-        <View style={{flex: 1}}>
-          <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
-            <Trans>User Lists</Trans>
-          </Text>
-          <Text style={pal.textLight}>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content align="left">
+          <Layout.Header.TitleText>
+            <Trans>Lists</Trans>
+          </Layout.Header.TitleText>
+          <Layout.Header.SubtitleText>
             <Trans>Public, shareable lists which can drive feeds.</Trans>
-          </Text>
-        </View>
-        <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
-          <Button
-            testID="newUserListBtn"
-            type="default"
-            onPress={onPressNewList}
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-              gap: 8,
-            }}>
-            <FontAwesomeIcon icon="plus" color={pal.colors.text} />
-            <Text type="button" style={pal.text}>
-              <Trans context="action">New</Trans>
-            </Text>
-          </Button>
-        </View>
-      </SimpleViewHeader>
-      <MyLists filter="curate" style={s.flexGrow1} />
+          </Layout.Header.SubtitleText>
+        </Layout.Header.Content>
+        <Button
+          label={_(msg`New list`)}
+          testID="newUserListBtn"
+          color="secondary"
+          variant="solid"
+          size="small"
+          onPress={onPressNewList}>
+          <ButtonIcon icon={PlusIcon} />
+          <ButtonText>
+            <Trans context="action">New</Trans>
+          </ButtonText>
+        </Button>
+      </Layout.Header.Outer>
+      <MyLists filter="curate" style={a.flex_grow} />
       <VerifyEmailDialog
         reasonText={_(
           msg`Before creating a list, you must first verify your email.`,
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index 53e31d1d2..9b0f54984 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -23,7 +23,6 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {Text} from '#/view/com/util/text/Text'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<
@@ -97,14 +96,7 @@ export function ModerationBlockedAccounts({}: Props) {
   )
   return (
     <Layout.Screen testID="blockedAccountsScreen">
-      <CenteredView
-        style={[
-          styles.container,
-          isTabletOrDesktop && styles.containerDesktop,
-          pal.view,
-          pal.border,
-        ]}
-        testID="blockedAccountsScreen">
+      <Layout.Center>
         <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
         <Text
           type="sm"
@@ -112,6 +104,9 @@ export function ModerationBlockedAccounts({}: Props) {
             styles.description,
             pal.text,
             isTabletOrDesktop && styles.descriptionDesktop,
+            {
+              marginTop: 20,
+            },
           ]}>
           <Trans>
             Blocked accounts cannot reply in your threads, mention you, or
@@ -120,7 +115,7 @@ export function ModerationBlockedAccounts({}: Props) {
           </Trans>
         </Text>
         {isEmpty ? (
-          <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+          <View style={[pal.border]}>
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -166,21 +161,12 @@ export function ModerationBlockedAccounts({}: Props) {
             desktopFixedHeight
           />
         )}
-      </CenteredView>
+      </Layout.Center>
     </Layout.Screen>
   )
 }
 
 const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: 100,
-  },
-  containerDesktop: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-    paddingBottom: 0,
-  },
   title: {
     textAlign: 'center',
     marginTop: 12,
diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx
index c623c5376..0ef4d4389 100644
--- a/src/view/screens/ModerationModlists.tsx
+++ b/src/view/screens/ModerationModlists.tsx
@@ -1,33 +1,26 @@
 import React from 'react'
-import {View} from 'react-native'
 import {AtUri} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 
 import {useEmail} from '#/lib/hooks/useEmail'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {NavigationProp} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
 import {useModalControls} from '#/state/modals'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {MyLists} from '#/view/com/lists/MyLists'
-import {Button} from '#/view/com/util/forms/Button'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {Text} from '#/view/com/util/text/Text'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {useDialogControl} from '#/components/Dialog'
 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
 export function ModerationModlistsScreen({}: Props) {
   const {_} = useLingui()
-  const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isMobile} = useWebMediaQueries()
   const navigation = useNavigation<NavigationProp>()
   const {openModal} = useModalControls()
   const {needsEmailVerification} = useEmail()
@@ -62,39 +55,32 @@ export function ModerationModlistsScreen({}: Props) {
 
   return (
     <Layout.Screen testID="moderationModlistsScreen">
-      <SimpleViewHeader
-        showBackButton={isMobile}
-        style={
-          !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
-        }>
-        <View style={{flex: 1}}>
-          <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content align="left">
+          <Layout.Header.TitleText>
             <Trans>Moderation Lists</Trans>
-          </Text>
-          <Text style={pal.textLight}>
+          </Layout.Header.TitleText>
+          <Layout.Header.SubtitleText>
             <Trans>
               Public, shareable lists of users to mute or block in bulk.
             </Trans>
-          </Text>
-        </View>
-        <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
-          <Button
-            testID="newModListBtn"
-            type="default"
-            onPress={onPressNewList}
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-              gap: 8,
-            }}>
-            <FontAwesomeIcon icon="plus" color={pal.colors.text} />
-            <Text type="button" style={pal.text}>
-              <Trans>New</Trans>
-            </Text>
-          </Button>
-        </View>
-      </SimpleViewHeader>
-      <MyLists filter="mod" style={s.flexGrow1} />
+          </Layout.Header.SubtitleText>
+        </Layout.Header.Content>
+        <Button
+          label={_(msg`New list`)}
+          testID="newModListBtn"
+          color="secondary"
+          variant="solid"
+          size="small"
+          onPress={onPressNewList}>
+          <ButtonIcon icon={PlusIcon} />
+          <ButtonText>
+            <Trans context="action">New</Trans>
+          </ButtonText>
+        </Button>
+      </Layout.Header.Outer>
+      <MyLists filter="mod" style={a.flex_grow} />
       <VerifyEmailDialog
         reasonText={_(
           msg`Before creating a list, you must first verify your email.`,
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 6d34c8a5f..6a8c6c3e6 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -23,7 +23,6 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {Text} from '#/view/com/util/text/Text'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<
@@ -97,21 +96,17 @@ export function ModerationMutedAccounts({}: Props) {
   )
   return (
     <Layout.Screen testID="mutedAccountsScreen">
-      <CenteredView
-        style={[
-          styles.container,
-          isTabletOrDesktop && styles.containerDesktop,
-          pal.view,
-          pal.border,
-        ]}
-        testID="mutedAccountsScreen">
-        <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
+      <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
+      <Layout.Center>
         <Text
           type="sm"
           style={[
             styles.description,
             pal.text,
             isTabletOrDesktop && styles.descriptionDesktop,
+            {
+              marginTop: 20,
+            },
           ]}>
           <Trans>
             Muted accounts have their posts removed from your feed and from your
@@ -119,7 +114,7 @@ export function ModerationMutedAccounts({}: Props) {
           </Trans>
         </Text>
         {isEmpty ? (
-          <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+          <View style={[pal.border]}>
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -165,21 +160,12 @@ export function ModerationMutedAccounts({}: Props) {
             desktopFixedHeight
           />
         )}
-      </CenteredView>
+      </Layout.Center>
     </Layout.Screen>
   )
 }
 
 const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: 100,
-  },
-  containerDesktop: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-    paddingBottom: 0,
-  },
   title: {
     textAlign: 'center',
     marginTop: 12,
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 531d10a7f..35591f270 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import React from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -6,7 +6,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {ComposeIcon2} from '#/lib/icons'
 import {
   NativeStackScreenProps,
@@ -14,7 +13,7 @@ import {
 } from '#/lib/routes/types'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
 import {emitSoftReset, listenSoftReset} from '#/state/events'
 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
 import {
@@ -29,28 +28,25 @@ import {FAB} from '#/view/com/util/fab/FAB'
 import {ListMethods} from '#/view/com/util/List'
 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
 import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
 import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
 import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
 export function NotificationsScreen({route: {params}}: Props) {
+  const t = useTheme()
+  const {gtTablet} = useBreakpoints()
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const [isScrolledDown, setIsScrolledDown] = React.useState(false)
   const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
   const scrollElRef = React.useRef<ListMethods>(null)
-  const t = useTheme()
-  const {isDesktop} = useWebMediaQueries()
   const queryClient = useQueryClient()
   const unreadNotifs = useUnreadNotifications()
   const unreadApi = useUnreadNotificationsApi()
@@ -110,121 +106,77 @@ export function NotificationsScreen({route: {params}}: Props) {
     return listenSoftReset(onPressLoadLatest)
   }, [onPressLoadLatest, isScreenFocused])
 
-  const renderButton = useCallback(() => {
-    return (
-      <Link
-        to="/notifications/settings"
-        label={_(msg`Notification settings`)}
-        size="small"
-        variant="ghost"
-        color="secondary"
-        shape="square"
-        style={[a.justify_center]}>
-        <SettingsIcon size="md" style={t.atoms.text_contrast_medium} />
-      </Link>
-    )
-  }, [_, t])
-
-  const ListHeaderComponent = React.useCallback(() => {
-    if (isDesktop) {
-      return (
-        <View
-          style={[
-            t.atoms.bg,
-            a.flex_row,
-            a.align_center,
-            a.justify_between,
-            a.gap_lg,
-            a.px_lg,
-            a.pr_md,
-            a.py_sm,
-          ]}>
+  return (
+    <Layout.Screen testID="notificationsScreen">
+      <Layout.Header.Outer>
+        <Layout.Header.MenuButton />
+        <Layout.Header.Content>
           <Button
             label={_(msg`Notifications`)}
             accessibilityHint={_(msg`Refresh notifications`)}
-            onPress={emitSoftReset}>
-            {({hovered, pressed}) => (
-              <Text
-                style={[
-                  a.text_2xl,
-                  a.font_bold,
-                  (hovered || pressed) && a.underline,
-                ]}>
+            onPress={emitSoftReset}
+            style={[a.justify_start]}>
+            {({hovered}) => (
+              <Layout.Header.TitleText
+                style={[a.w_full, hovered && a.underline]}>
                 <Trans>Notifications</Trans>
-                {hasNew && (
+                {isWeb && gtTablet && hasNew && (
                   <View
-                    style={{
-                      left: 4,
-                      top: -8,
-                      backgroundColor: t.palette.primary_500,
-                      width: 8,
-                      height: 8,
-                      borderRadius: 4,
-                    }}
+                    style={[
+                      a.rounded_full,
+                      {
+                        width: 8,
+                        height: 8,
+                        bottom: 3,
+                        left: 6,
+                        backgroundColor: t.palette.primary_500,
+                      },
+                    ]}
                   />
                 )}
-              </Text>
+              </Layout.Header.TitleText>
             )}
           </Button>
-          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-            {isLoadingLatest ? <Loader size="md" /> : <></>}
-            {renderButton()}
-          </View>
-        </View>
-      )
-    }
-    return <></>
-  }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
-
-  const renderHeaderSpinner = React.useCallback(() => {
-    return (
-      <View
-        style={[
-          {width: 30, height: 20},
-          a.flex_row,
-          a.align_center,
-          a.justify_end,
-          a.gap_md,
-        ]}>
-        {isLoadingLatest ? <Loader width={20} /> : <></>}
-        {renderButton()}
-      </View>
-    )
-  }, [renderButton, isLoadingLatest])
+        </Layout.Header.Content>
+        <Layout.Header.Slot>
+          <Link
+            to="/notifications/settings"
+            label={_(msg`Notification settings`)}
+            size="small"
+            variant="ghost"
+            color="secondary"
+            shape="round"
+            style={[a.justify_center]}>
+            <ButtonIcon
+              icon={isLoadingLatest ? Loader : SettingsIcon}
+              size="lg"
+            />
+          </Link>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
 
-  return (
-    <Layout.Screen testID="notificationsScreen">
-      <CenteredView style={[a.flex_1, {paddingTop: 2}]} sideBorders={true}>
-        <ViewHeader
-          title={_(msg`Notifications`)}
-          canGoBack={false}
-          showBorder={true}
-          renderButton={renderHeaderSpinner}
+      <MainScrollProvider>
+        <Feed
+          onScrolledDownChange={setIsScrolledDown}
+          scrollElRef={scrollElRef}
+          overridePriorityNotifications={params?.show === 'all'}
         />
-        <MainScrollProvider>
-          <Feed
-            onScrolledDownChange={setIsScrolledDown}
-            scrollElRef={scrollElRef}
-            ListHeaderComponent={ListHeaderComponent}
-            overridePriorityNotifications={params?.show === 'all'}
-          />
-        </MainScrollProvider>
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onPressLoadLatest}
-            label={_(msg`Load new notifications`)}
-            showIndicator={hasNew}
-          />
-        )}
-        <FAB
-          testID="composeFAB"
-          onPress={() => openComposer({})}
-          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`New post`)}
-          accessibilityHint=""
+      </MainScrollProvider>
+      {(isScrolledDown || hasNew) && (
+        <LoadLatestBtn
+          onPress={onPressLoadLatest}
+          label={_(msg`Load new notifications`)}
+          showIndicator={hasNew}
         />
-      </CenteredView>
+      )}
+      <FAB
+        testID="composeFAB"
+        onPress={() => openComposer({})}
+        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`New post`)}
+        accessibilityHint=""
+      />
     </Layout.Screen>
   )
 }
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index c183569b7..1bad9b6cd 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -1,12 +1,10 @@
 import React from 'react'
-import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {makeRecordUri} from '#/lib/strings/url-helpers'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread'
-import {atoms as a} from '#/alf'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
@@ -24,9 +22,7 @@ export function PostThreadScreen({route}: Props) {
 
   return (
     <Layout.Screen testID="postThreadScreen">
-      <View style={a.flex_1}>
-        <PostThreadComponent uri={uri} />
-      </View>
+      <PostThreadComponent uri={uri} />
     </Layout.Screen>
   )
 }
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 677fe09f4..6a9b6f7f2 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -40,11 +40,9 @@ import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {FAB} from '#/view/com/util/fab/FAB'
 import {ListRef} from '#/view/com/util/List'
-import {CenteredView} from '#/view/com/util/Views'
 import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
-import {web} from '#/alf'
 import * as Layout from '#/components/Layout'
 import {ScreenHider} from '#/components/moderation/ScreenHider'
 import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
@@ -116,9 +114,9 @@ function ProfileScreenInner({route}: Props) {
   // Most pushes will happen here, since we will have only placeholder data
   if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) {
     return (
-      <CenteredView sideBorders style={web({height: '100vh'})}>
+      <Layout.Content>
         <ProfileHeaderLoading />
-      </CenteredView>
+      </Layout.Content>
     )
   }
   if (resolveError || profileError) {
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index b34f0f1b0..63469ef4f 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -49,7 +49,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
 import {LoadingScreen} from '#/view/com/util/LoadingScreen'
 import {Text} from '#/view/com/util/text/Text'
 import * as Toast from '#/view/com/util/Toast'
-import {CenteredView} from '#/view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
 import {Button as NewButton, ButtonText} from '#/components/Button'
 import {useRichText} from '#/components/hooks/useRichText'
@@ -98,7 +97,7 @@ export function ProfileFeedScreen(props: Props) {
   if (error) {
     return (
       <Layout.Screen testID="profileFeedScreenError">
-        <CenteredView>
+        <Layout.Content>
           <View style={[pal.view, pal.border, styles.notFoundContainer]}>
             <Text type="title-lg" style={[pal.text, s.mb10]}>
               <Trans>Could not load feed</Trans>
@@ -120,7 +119,7 @@ export function ProfileFeedScreen(props: Props) {
               </Button>
             </View>
           </View>
-        </CenteredView>
+        </Layout.Content>
       </Layout.Screen>
     )
   }
@@ -394,7 +393,7 @@ export function ProfileFeedScreenInner({
   ])
 
   return (
-    <View style={s.hContentRegion}>
+    <>
       <ReportDialog
         control={reportDialogControl}
         params={{
@@ -434,7 +433,7 @@ export function ProfileFeedScreenInner({
           accessibilityHint=""
         />
       )}
-    </View>
+    </>
   )
 }
 
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 9fa98cb1a..90c0a57f9 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -10,7 +10,6 @@ import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
-import {ListHeaderDesktop} from '#/components/Lists'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
 export const ProfileFollowersScreen = ({route}: Props) => {
@@ -27,7 +26,6 @@ export const ProfileFollowersScreen = ({route}: Props) => {
   return (
     <Layout.Screen testID="profileFollowersScreen">
       <CenteredView sideBorders={true}>
-        <ListHeaderDesktop title={_(msg`Followers`)} />
         <ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} />
         <ProfileFollowersComponent name={name} />
       </CenteredView>
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 483ee93ec..134f79993 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -10,7 +10,6 @@ import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/Prof
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {CenteredView} from '#/view/com/util/Views'
 import * as Layout from '#/components/Layout'
-import {ListHeaderDesktop} from '#/components/Lists'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
 export const ProfileFollowsScreen = ({route}: Props) => {
@@ -27,7 +26,6 @@ export const ProfileFollowsScreen = ({route}: Props) => {
   return (
     <Layout.Screen testID="profileFollowsScreen">
       <CenteredView sideBorders={true}>
-        <ListHeaderDesktop title={_(msg`Following`)} />
         <ViewHeader title={_(msg`Following`)} showBorder={!isWeb} />
         <ProfileFollowsComponent name={name} />
       </CenteredView>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index cb333befa..a927526ad 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -69,7 +69,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
 import {LoadingScreen} from '#/view/com/util/LoadingScreen'
 import {Text} from '#/view/com/util/text/Text'
 import * as Toast from '#/view/com/util/Toast'
-import {CenteredView} from '#/view/com/util/Views'
 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
 import {atoms as a, useTheme} from '#/alf'
 import {useDialogControl} from '#/components/Dialog'
@@ -107,20 +106,20 @@ function ProfileListScreenInner(props: Props) {
 
   if (resolveError) {
     return (
-      <CenteredView>
+      <Layout.Content>
         <ErrorScreen
           error={_(
             msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
           )}
         />
-      </CenteredView>
+      </Layout.Content>
     )
   }
   if (listError) {
     return (
-      <CenteredView>
+      <Layout.Content>
         <ErrorScreen error={cleanError(listError)} />
-      </CenteredView>
+      </Layout.Content>
     )
   }
 
@@ -1010,7 +1009,6 @@ function ErrorScreen({error}: {error: string}) {
         pal.view,
         pal.border,
         {
-          marginTop: 10,
           paddingHorizontal: 18,
           paddingVertical: 14,
           borderTopWidth: StyleSheet.hairlineWidth,
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 3c04ec36f..1b4c84a60 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -25,13 +25,12 @@ import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
 import {TextLink} from '#/view/com/util/Link'
 import {Text} from '#/view/com/util/text/Text'
 import * as Toast from '#/view/com/util/Toast'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
+import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
 import * as Layout from '#/components/Layout'
 import {Loader} from '#/components/Loader'
 
@@ -51,7 +50,7 @@ function SavedFeedsInner({
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries()
+  const {isMobile, isDesktop} = useWebMediaQueries()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} =
     useOverwriteSavedFeedsMutation()
@@ -88,136 +87,128 @@ function SavedFeedsInner({
     }
   }, [_, overwriteSavedFeeds, currentFeeds, navigation])
 
-  const renderHeaderBtn = React.useCallback(() => {
-    return (
-      <Button
-        size="small"
-        variant={hasUnsavedChanges ? 'solid' : 'solid'}
-        color={hasUnsavedChanges ? 'primary' : 'secondary'}
-        onPress={onSaveChanges}
-        label={_(msg`Save changes`)}
-        disabled={isOverwritePending || !hasUnsavedChanges}
-        style={[isDesktop && a.mt_sm]}
-        testID="saveChangesBtn">
-        <ButtonText style={[isDesktop && a.text_md]}>
-          {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>}
-        </ButtonText>
-        {isOverwritePending && <ButtonIcon icon={Loader} />}
-      </Button>
-    )
-  }, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending])
-
   return (
     <Layout.Screen>
-      <CenteredView
-        style={[a.util_screen_outer]}
-        sideBorders={isTabletOrDesktop}>
-        <ViewHeader
-          title={_(msg`Edit My Feeds`)}
-          showOnDesktop
-          showBorder
-          renderButton={renderHeaderBtn}
-        />
-        <ScrollView style={[a.flex_1]} contentContainerStyle={[a.border_0]}>
-          {noSavedFeedsOfAnyType && (
-            <View style={[pal.border, a.border_b]}>
-              <NoSavedFeedsOfAnyType />
-            </View>
-          )}
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content align="left">
+          <Layout.Header.TitleText>
+            <Trans>Feeds</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Button
+          testID="saveChangesBtn"
+          size="small"
+          variant={hasUnsavedChanges ? 'solid' : 'solid'}
+          color={hasUnsavedChanges ? 'primary' : 'secondary'}
+          onPress={onSaveChanges}
+          label={_(msg`Save changes`)}
+          disabled={isOverwritePending || !hasUnsavedChanges}>
+          <ButtonIcon icon={isOverwritePending ? Loader : SaveIcon} />
+          <ButtonText>
+            {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>}
+          </ButtonText>
+        </Button>
+      </Layout.Header.Outer>
 
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              <Trans>Pinned Feeds</Trans>
-            </Text>
+      <Layout.Content>
+        {noSavedFeedsOfAnyType && (
+          <View style={[pal.border, a.border_b]}>
+            <NoSavedFeedsOfAnyType />
           </View>
+        )}
 
-          {preferences ? (
-            !pinnedFeeds.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  <Trans>You don't have any pinned feeds.</Trans>
-                </Text>
-              </View>
-            ) : (
-              pinnedFeeds.map(f => (
-                <ListItem
-                  key={f.id}
-                  feed={f}
-                  isPinned
-                  currentFeeds={currentFeeds}
-                  setCurrentFeeds={setCurrentFeeds}
-                  preferences={preferences}
-                />
-              ))
-            )
-          ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            <Trans>Pinned Feeds</Trans>
+          </Text>
+        </View>
 
-          {noFollowingFeed && (
-            <View style={[pal.border, a.border_b]}>
-              <NoFollowingFeed />
+        {preferences ? (
+          !pinnedFeeds.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                <Trans>You don't have any pinned feeds.</Trans>
+              </Text>
             </View>
-          )}
+          ) : (
+            pinnedFeeds.map(f => (
+              <ListItem
+                key={f.id}
+                feed={f}
+                isPinned
+                currentFeeds={currentFeeds}
+                setCurrentFeeds={setCurrentFeeds}
+                preferences={preferences}
+              />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
 
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              <Trans>Saved Feeds</Trans>
-            </Text>
+        {noFollowingFeed && (
+          <View style={[pal.border, a.border_b]}>
+            <NoFollowingFeed />
           </View>
-          {preferences ? (
-            !unpinnedFeeds.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  <Trans>You don't have any saved feeds.</Trans>
-                </Text>
-              </View>
-            ) : (
-              unpinnedFeeds.map(f => (
-                <ListItem
-                  key={f.id}
-                  feed={f}
-                  isPinned={false}
-                  currentFeeds={currentFeeds}
-                  setCurrentFeeds={setCurrentFeeds}
-                  preferences={preferences}
-                />
-              ))
-            )
+        )}
+
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            <Trans>Saved Feeds</Trans>
+          </Text>
+        </View>
+        {preferences ? (
+          !unpinnedFeeds.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                <Trans>You don't have any saved feeds.</Trans>
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
+            unpinnedFeeds.map(f => (
+              <ListItem
+                key={f.id}
+                feed={f}
+                isPinned={false}
+                currentFeeds={currentFeeds}
+                setCurrentFeeds={setCurrentFeeds}
+                preferences={preferences}
+              />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
 
-          <View style={styles.footerText}>
-            <Text type="sm" style={pal.textLight}>
-              <Trans>
-                Feeds are custom algorithms that users build with a little
-                coding expertise.{' '}
-                <TextLink
-                  type="sm"
-                  style={pal.link}
-                  href="https://github.com/bluesky-social/feed-generator"
-                  text={_(msg`See this guide`)}
-                />{' '}
-                for more information.
-              </Trans>
-            </Text>
-          </View>
-          <View style={{height: 100}} />
-        </ScrollView>
-      </CenteredView>
+        <View style={styles.footerText}>
+          <Text type="sm" style={pal.textLight}>
+            <Trans>
+              Feeds are custom algorithms that users build with a little coding
+              expertise.{' '}
+              <TextLink
+                type="sm"
+                style={pal.link}
+                href="https://github.com/bluesky-social/feed-generator"
+                text={_(msg`See this guide`)}
+              />{' '}
+              for more information.
+            </Trans>
+          </Text>
+        </View>
+      </Layout.Content>
     </Layout.Screen>
   )
 }
@@ -456,7 +447,6 @@ const styles = StyleSheet.create({
   },
   footerText: {
     paddingHorizontal: 26,
-    paddingTop: 22,
-    paddingBottom: 100,
+    paddingVertical: 22,
   },
 })
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 0518bc506..0871458c9 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -55,7 +55,6 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {Link} from '#/view/com/util/Link'
 import {List} from '#/view/com/util/List'
 import {Text} from '#/view/com/util/text/Text'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
 import {Explore} from '#/view/screens/Search/Explore'
 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
 import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
@@ -68,63 +67,46 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
 import * as Layout from '#/components/Layout'
 
 function Loader() {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
   return (
-    <CenteredView
-      style={[
-        // @ts-ignore web only -prf
-        {
-          padding: 18,
-          height: isWeb ? '100vh' : undefined,
-        },
-        pal.border,
-      ]}
-      sideBorders={!isMobile}>
-      <ActivityIndicator />
-    </CenteredView>
+    <Layout.Content>
+      <View style={[a.py_xl]}>
+        <ActivityIndicator />
+      </View>
+    </Layout.Content>
   )
 }
 
 function EmptyState({message, error}: {message: string; error?: string}) {
   const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
 
   return (
-    <CenteredView
-      sideBorders={!isMobile}
-      style={[
-        pal.border,
-        // @ts-ignore web only -prf
-        {
-          padding: 18,
-          height: isWeb ? '100vh' : undefined,
-        },
-      ]}>
-      <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
-        <Text style={[pal.text]}>{message}</Text>
+    <Layout.Content>
+      <View style={[a.p_xl]}>
+        <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
+          <Text style={[pal.text]}>{message}</Text>
 
-        {error && (
-          <>
-            <View
-              style={[
-                {
-                  marginVertical: 12,
-                  height: 1,
-                  width: '100%',
-                  backgroundColor: pal.text.color,
-                  opacity: 0.2,
-                },
-              ]}
-            />
+          {error && (
+            <>
+              <View
+                style={[
+                  {
+                    marginVertical: 12,
+                    height: 1,
+                    width: '100%',
+                    backgroundColor: pal.text.color,
+                    opacity: 0.2,
+                  },
+                ]}
+              />
 
-            <Text style={[pal.textLight]}>
-              <Trans>Error:</Trans> {error}
-            </Text>
-          </>
-        )}
+              <Text style={[pal.textLight]}>
+                <Trans>Error:</Trans> {error}
+              </Text>
+            </>
+          )}
+        </View>
       </View>
-    </CenteredView>
+    </Layout.Content>
   )
 }
 
@@ -224,7 +206,7 @@ let SearchScreenPostResults = ({
                 if (item.type === 'post') {
                   return <Post post={item.post} />
                 } else {
-                  return <Loader />
+                  return null
                 }
               }}
               keyExtractor={item => item.key}
@@ -550,19 +532,13 @@ let SearchScreenInner = ({
     <Pager
       onPageSelected={onPageSelected}
       renderTabBar={props => (
-        <CenteredView
-          sideBorders
+        <Layout.Center
           style={[
-            pal.border,
-            pal.view,
-            web({
-              position: isWeb ? 'sticky' : '',
-              zIndex: 1,
-            }),
+            web([a.sticky, a.z_10]),
             {top: isWeb ? headerHeight : undefined},
           ]}>
           <TabBar items={sections.map(section => section.title)} {...props} />
-        </CenteredView>
+        </Layout.Center>
       )}
       initialPage={0}>
       {sections.map((section, i) => (
@@ -572,7 +548,7 @@ let SearchScreenInner = ({
   ) : hasSession ? (
     <Explore />
   ) : (
-    <CenteredView sideBorders style={pal.border}>
+    <Layout.Center>
       <View
         // @ts-ignore web only -esb
         style={{
@@ -614,7 +590,7 @@ let SearchScreenInner = ({
           </Text>
         </View>
       </View>
-    </CenteredView>
+    </Layout.Center>
   )
 }
 SearchScreenInner = React.memo(SearchScreenInner)
@@ -650,7 +626,7 @@ export function SearchScreen(
    * Arbitrary sizing, so guess and check, used for sticky header alignment and
    * sizing.
    */
-  const headerHeight = 64 + (showFilters ? 40 : 0)
+  const headerHeight = 60 + (showFilters ? 40 : 0)
 
   useFocusEffect(
     useNonReactiveCallback(() => {
@@ -861,73 +837,79 @@ export function SearchScreen(
 
   return (
     <Layout.Screen testID="searchScreen">
-      <CenteredView
+      <View
         style={[
-          a.p_md,
-          a.pb_sm,
-          a.gap_sm,
-          t.atoms.bg,
           web({
             height: headerHeight,
             position: 'sticky',
             top: 0,
             zIndex: 1,
           }),
-        ]}
-        sideBorders={gtMobile}>
-        <View style={[a.flex_row, a.gap_sm]}>
-          {!gtMobile && (
-            <Button
-              testID="viewHeaderBackOrMenuBtn"
-              onPress={onPressMenu}
-              hitSlop={HITSLOP_10}
-              label={_(msg`Menu`)}
-              accessibilityHint={_(msg`Access navigation links and settings`)}
-              size="large"
-              variant="solid"
-              color="secondary"
-              shape="square">
-              <ButtonIcon icon={Menu} size="lg" />
-            </Button>
-          )}
-          <View style={[a.flex_1]}>
-            <SearchInput
-              ref={textInput}
-              value={searchText}
-              onFocus={onSearchInputFocus}
-              onChangeText={onChangeText}
-              onClearText={onPressClearQuery}
-              onSubmitEditing={onSubmit}
-            />
-          </View>
-          {showAutocomplete && (
-            <Button
-              label={_(msg`Cancel search`)}
-              size="large"
-              variant="ghost"
-              color="secondary"
-              style={[a.px_sm]}
-              onPress={onPressCancelSearch}
-              hitSlop={HITSLOP_10}>
-              <ButtonText>
-                <Trans>Cancel</Trans>
-              </ButtonText>
-            </Button>
-          )}
-        </View>
-
-        {showFilters && (
-          <View
-            style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}>
-            <View style={[{width: 140}]}>
-              <SearchLanguageDropdown
-                value={params.lang}
-                onChange={params.setLang}
-              />
+        ]}>
+        <Layout.Center>
+          <View style={[a.p_md, a.pb_sm, a.gap_sm, t.atoms.bg]}>
+            <View style={[a.flex_row, a.gap_sm]}>
+              {!gtMobile && (
+                <Button
+                  testID="viewHeaderBackOrMenuBtn"
+                  onPress={onPressMenu}
+                  hitSlop={HITSLOP_10}
+                  label={_(msg`Menu`)}
+                  accessibilityHint={_(
+                    msg`Access navigation links and settings`,
+                  )}
+                  size="large"
+                  variant="solid"
+                  color="secondary"
+                  shape="square">
+                  <ButtonIcon icon={Menu} size="lg" />
+                </Button>
+              )}
+              <View style={[a.flex_1]}>
+                <SearchInput
+                  ref={textInput}
+                  value={searchText}
+                  onFocus={onSearchInputFocus}
+                  onChangeText={onChangeText}
+                  onClearText={onPressClearQuery}
+                  onSubmitEditing={onSubmit}
+                />
+              </View>
+              {showAutocomplete && (
+                <Button
+                  label={_(msg`Cancel search`)}
+                  size="large"
+                  variant="ghost"
+                  color="secondary"
+                  style={[a.px_sm]}
+                  onPress={onPressCancelSearch}
+                  hitSlop={HITSLOP_10}>
+                  <ButtonText>
+                    <Trans>Cancel</Trans>
+                  </ButtonText>
+                </Button>
+              )}
             </View>
+
+            {showFilters && (
+              <View
+                style={[
+                  a.flex_row,
+                  a.align_center,
+                  a.justify_between,
+                  a.gap_sm,
+                ]}>
+                <View style={[{width: 140}]}>
+                  <SearchLanguageDropdown
+                    value={params.lang}
+                    onChange={params.setLang}
+                  />
+                </View>
+              </View>
+            )}
           </View>
-        )}
-      </CenteredView>
+        </Layout.Center>
+      </View>
 
       <View
         style={{
@@ -992,10 +974,7 @@ let AutocompleteResults = ({
       !moderationOpts ? (
         <Loader />
       ) : (
-        <ScrollView
-          style={{height: '100%'}}
-          // @ts-ignore web only -prf
-          dataSet={{stableGutters: '1'}}
+        <Layout.Content
           keyboardShouldPersistTaps="handled"
           keyboardDismissMode="on-drag">
           <SearchLinkCard
@@ -1020,7 +999,7 @@ let AutocompleteResults = ({
             />
           ))}
           <View style={{height: 200}} />
-        </ScrollView>
+        </Layout.Content>
       )}
     </>
   )
@@ -1042,17 +1021,12 @@ function SearchHistory({
   onRemoveItemClick: (item: string) => void
   onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
 }) {
-  const {isTabletOrDesktop, isMobile} = useWebMediaQueries()
+  const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
   const {_} = useLingui()
 
   return (
-    <CenteredView
-      sideBorders={isTabletOrDesktop}
-      // @ts-ignore web only -prf
-      style={{
-        height: isWeb ? '100vh' : undefined,
-      }}>
+    <Layout.Content>
       <View style={styles.searchHistoryContainer}>
         {(searchHistory.length > 0 || selectedProfiles.length > 0) && (
           <Text style={[pal.text, styles.searchHistoryTitle]}>
@@ -1152,7 +1126,7 @@ function SearchHistory({
           </View>
         )}
       </View>
-    </CenteredView>
+    </Layout.Content>
   )
 }
 
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 9f407248a..47a86ed24 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -3,8 +3,8 @@ import {StyleSheet, View} from 'react-native'
 import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
 import {useFocusGuards} from '@radix-ui/react-focus-guards'
 import {FocusScope} from '@radix-ui/react-focus-scope'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useModals} from '#/state/modals'
 import {ComposerOpts, useComposerState} from '#/state/shell/composer'
 import {
@@ -20,8 +20,6 @@ export function Composer({}: {winHeight: number}) {
   const state = useComposerState()
   const isActive = !!state
 
-  useWebBodyScrollLock(isActive)
-
   // rendering
   // =
 
@@ -29,7 +27,12 @@ export function Composer({}: {winHeight: number}) {
     return <View />
   }
 
-  return <Inner state={state} />
+  return (
+    <>
+      <RemoveScrollBar />
+      <Inner state={state} />
+    </>
+  )
 }
 
 function Inner({state}: {state: ComposerOpts}) {
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 0af80854c..7c2ccd958 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -1,9 +1,6 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
+import {StyleSheet, View} from 'react-native'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
@@ -14,9 +11,9 @@ import {
 
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
+import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
 import {makeProfileLink} from '#/lib/routes/links'
-import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
+import {CommonNavigatorParams} from '#/lib/routes/types'
 import {isInvalidHandle} from '#/lib/strings/handles'
 import {emitSoftReset} from '#/state/events'
 import {useFetchHandle} from '#/state/queries/handle'
@@ -101,47 +98,6 @@ function ProfileCard() {
   )
 }
 
-const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
-
-function BackBtn() {
-  const {isTablet} = useWebMediaQueries()
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
-  const {_} = useLingui()
-  const shouldShow = useNavigationState(
-    state =>
-      !isStateAtTabRoot(state) &&
-      !HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
-  )
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  if (!shouldShow || isTablet) {
-    return <></>
-  }
-  return (
-    <TouchableOpacity
-      testID="viewHeaderBackOrMenuBtn"
-      onPress={onPressBack}
-      style={styles.backBtn}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Go back`)}
-      accessibilityHint="">
-      <FontAwesomeIcon
-        size={24}
-        icon="angle-left"
-        style={pal.text as FontAwesomeIconStyle}
-      />
-    </TouchableOpacity>
-  )
-}
-
 interface NavItemProps {
   count?: string
   href: string
@@ -220,35 +176,44 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
         ]}>
         {isCurrent ? iconFilled : icon}
         {typeof count === 'string' && count ? (
-          <Text
-            accessibilityLabel={_(msg`${count} unread items`)}
-            accessibilityHint=""
-            accessible={true}
+          <View
             style={[
               a.absolute,
-              a.text_xs,
-              a.font_bold,
-              a.rounded_full,
-              a.text_center,
-              {
-                top: '-10%',
-                left: count.length === 1 ? '50%' : '40%',
-                backgroundColor: t.palette.primary_500,
-                color: t.palette.white,
-                lineHeight: a.text_sm.fontSize,
-                paddingHorizontal: 4,
-                paddingVertical: 1,
-                minWidth: 16,
-              },
-              isTablet && [
+              a.inset_0,
+              {right: -20}, // more breathing room
+            ]}>
+            <Text
+              accessibilityLabel={_(msg`${count} unread items`)}
+              accessibilityHint=""
+              accessible={true}
+              numberOfLines={1}
+              style={[
+                a.absolute,
+                a.text_xs,
+                a.font_bold,
+                a.rounded_full,
+                a.text_center,
+                a.leading_tight,
                 {
-                  top: '10%',
-                  left: count.length === 1 ? '50%' : '40%',
+                  top: '-10%',
+                  left: count.length === 1 ? 12 : 8,
+                  backgroundColor: t.palette.primary_500,
+                  color: t.palette.white,
+                  lineHeight: a.text_sm.fontSize,
+                  paddingHorizontal: 4,
+                  paddingVertical: 1,
+                  minWidth: 16,
                 },
-              ],
-            ]}>
-            {count}
-          </Text>
+                isTablet && [
+                  {
+                    top: '10%',
+                    left: count.length === 1 ? 20 : 16,
+                  },
+                ],
+              ]}>
+              {count}
+            </Text>
+          </View>
         ) : null}
       </View>
       {gtTablet && (
@@ -366,9 +331,9 @@ export function DesktopLeftNav() {
     <View
       role="navigation"
       style={[
+        a.px_xl,
         styles.leftNav,
         isTablet && styles.leftNavTablet,
-        pal.view,
         pal.border,
       ]}>
       {hasSession ? (
@@ -381,8 +346,6 @@ export function DesktopLeftNav() {
 
       {hasSession && (
         <>
-          <BackBtn />
-
           <NavItem
             href="/"
             icon={
@@ -525,8 +488,17 @@ const styles = StyleSheet.create({
     position: 'fixed',
     top: 10,
     // @ts-ignore web only
-    left: 'calc(50vw - 300px - 220px - 20px)',
-    width: 220,
+    left: '50%',
+    transform: [
+      {
+        translateX: -300,
+      },
+      {
+        translateX: '-100%',
+      },
+      ...a.scrollbar_offset.transform,
+    ],
+    width: 240,
     // @ts-ignore web only
     maxHeight: 'calc(100vh - 10px)',
     overflowY: 'auto',
@@ -538,7 +510,10 @@ const styles = StyleSheet.create({
     borderRightWidth: 1,
     height: '100%',
     width: 76,
+    paddingLeft: 0,
+    paddingRight: 0,
     alignItems: 'center',
+    transform: [],
   },
 
   profileCard: {
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 4f413211f..7814f3548 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -28,7 +28,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
   }
 
   return (
-    <View style={[styles.rightNav, pal.view]}>
+    <View style={[a.px_xl, styles.rightNav]}>
       <View style={{paddingVertical: 20}}>
         {routeName === 'Search' ? (
           <View style={{marginBottom: 18}}>
@@ -122,8 +122,13 @@ const styles = StyleSheet.create({
     // @ts-ignore web only
     position: 'fixed',
     // @ts-ignore web only
-    left: 'calc(50vw + 300px + 20px)',
-    width: 300,
+    left: '50%',
+    transform: [
+      {
+        translateX: 300,
+      },
+      ...a.scrollbar_offset.transform,
+    ],
     maxHeight: '100%',
     overflowY: 'auto',
   },
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index f55437356..8c30813ab 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -3,10 +3,10 @@ import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
 import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {NavigationProp} from '#/lib/routes/types'
 import {colors} from '#/lib/styles'
@@ -34,7 +34,6 @@ function ShellInner() {
   const {_} = useLingui()
   const showDrawer = !isDesktop && isDrawerOpen
 
-  useWebBodyScrollLock(showDrawer)
   useComposerKeyboardShortcut()
   useIntentHandler()
 
@@ -58,31 +57,34 @@ function ShellInner() {
       <PortalOutlet />
 
       {showDrawer && (
-        <TouchableWithoutFeedback
-          onPress={ev => {
-            // Only close if press happens outside of the drawer
-            if (ev.target === ev.currentTarget) {
-              setDrawerOpen(false)
-            }
-          }}
-          accessibilityLabel={_(msg`Close navigation footer`)}
-          accessibilityHint={_(msg`Closes bottom navigation bar`)}>
-          <View
-            style={[
-              styles.drawerMask,
-              {
-                backgroundColor: select(t.name, {
-                  light: 'rgba(0, 57, 117, 0.1)',
-                  dark: 'rgba(1, 82, 168, 0.1)',
-                  dim: 'rgba(10, 13, 16, 0.8)',
-                }),
-              },
-            ]}>
-            <View style={styles.drawerContainer}>
-              <DrawerContent />
+        <>
+          <RemoveScrollBar />
+          <TouchableWithoutFeedback
+            onPress={ev => {
+              // Only close if press happens outside of the drawer
+              if (ev.target === ev.currentTarget) {
+                setDrawerOpen(false)
+              }
+            }}
+            accessibilityLabel={_(msg`Close navigation footer`)}
+            accessibilityHint={_(msg`Closes bottom navigation bar`)}>
+            <View
+              style={[
+                styles.drawerMask,
+                {
+                  backgroundColor: select(t.name, {
+                    light: 'rgba(0, 57, 117, 0.1)',
+                    dark: 'rgba(1, 82, 168, 0.1)',
+                    dim: 'rgba(10, 13, 16, 0.8)',
+                  }),
+                },
+              ]}>
+              <View style={styles.drawerContainer}>
+                <DrawerContent />
+              </View>
             </View>
-          </View>
-        </TouchableWithoutFeedback>
+          </TouchableWithoutFeedback>
+        </>
       )}
     </>
   )
diff --git a/web/index.html b/web/index.html
index 28b3a3e3d..293f366ad 100644
--- a/web/index.html
+++ b/web/index.html
@@ -45,7 +45,6 @@
       }
       html {
         background-color: white;
-        scrollbar-gutter: stable both-edges;
       }
       @media (prefers-color-scheme: dark) {
         html {
@@ -81,9 +80,15 @@
         top: 50%;
         transform: translateX(-50%) translateY(-50%) translateY(-50px);
       }
-      /* We need this style to prevent web dropdowns from shifting the display when opening */
+      /**
+       * We need these styles to prevent shifting due to scrollbar show/hide on
+       * OSs that have them enabled by default. This also handles cases where the
+       * screen wouldn't otherwise scroll, and therefore hide the scrollbar and
+       * shift the content, by forcing the page to show a scrollbar.
+       */
       body {
         width: 100%;
+        overflow-y: scroll;
       }
     </style>
   </head>
diff --git a/yarn.lock b/yarn.lock
index 474ef2f8b..e62dcb97f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17616,16 +17616,7 @@ string-natural-compare@^3.0.1:
   resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
   integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 
-"string-width-cjs@npm:string-width@^4.2.0":
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -17725,7 +17716,7 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17739,13 +17730,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
   dependencies:
     ansi-regex "^4.1.0"
 
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
 strip-ansi@^7.0.1:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -19068,7 +19052,7 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -19086,15 +19070,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"