diff --git a/.vscode/settings.json b/.vscode/settings.json
index 476b51e..742316b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
 {
-  "cSpell.words": ["resumarkdown", "spacebar", "testid"]
+  "cSpell.words": ["resumarkdown", "spacebar", "tablist", "testid"]
 }
diff --git a/src/app.less b/src/app.less
index 09876d2..d48779c 100644
--- a/src/app.less
+++ b/src/app.less
@@ -12,7 +12,7 @@ body {
 }
 
 h1 {
-  font-weight: @weight-bold;
+  font-weight: @font-w-bold;
   margin: 0;
   padding: 0;
 }
diff --git a/src/lib/components/code-input.svelte b/src/lib/components/code-input.svelte
index 893e511..cc26983 100644
--- a/src/lib/components/code-input.svelte
+++ b/src/lib/components/code-input.svelte
@@ -15,12 +15,5 @@
     padding: @padding-lg;
     resize: none;
     width: .full-without-padding(@padding-lg-x) [];
-
-    @media screen and (min-width: @sizes[lg]) {
-      font-size: unset;
-      height: .full-without-padding(@padding-xl-y) [];
-      margin: @padding-xl;
-      width: .full-without-padding(@padding-xl-x) [];
-    }
   }
 </style>
diff --git a/src/lib/components/content.svelte b/src/lib/components/content.svelte
deleted file mode 100644
index 48173c5..0000000
--- a/src/lib/components/content.svelte
+++ /dev/null
@@ -1,54 +0,0 @@
-<script lang="ts">
-  import { navHeight } from '$lib/stores/layout';
-  import { pane as navStore, type Pane } from '$lib/stores/nav';
-
-  export let pane: Pane;
-
-  export let testid: string | undefined = undefined;
-
-  // don't show pane if we're not the current pane
-  let hidden: boolean;
-  $: hidden = pane !== $navStore;
-
-  // always show preview pane on a two column layout
-  let preview: boolean;
-  $: preview = pane === 'preview';
-
-  // offset preview pane upward on desktop
-  let offset: number;
-  $: offset = preview ? $navHeight : 0;
-</script>
-
-<div
-  id={`pane-${pane}`}
-  data-testid={testid}
-  role="tabpanel"
-  class:hidden
-  class:preview
-  style:--offset={`-${offset}px`}
->
-  <slot />
-</div>
-
-<style lang="less">
-  div {
-    box-sizing: border-box;
-    height: 100%;
-
-    &.hidden {
-      display: none;
-    }
-
-    @media screen and (min-width: @sizes[lg]) {
-      border-right: 2px solid @color-dark;
-      width: 50%;
-
-      &.preview {
-        border-right: unset !important;
-        display: unset !important;
-        height: calc(100% - var(--offset));
-        transform: translateY(var(--offset));
-      }
-    }
-  }
-</style>
diff --git a/src/lib/components/editor.svelte b/src/lib/components/editor.svelte
new file mode 100644
index 0000000..6471ced
--- /dev/null
+++ b/src/lib/components/editor.svelte
@@ -0,0 +1,25 @@
+<script lang="ts">
+  import { pane as navStore, type Pane } from '$lib/stores/nav';
+
+  export let pane: Pane;
+
+  export let testid: string | undefined = undefined;
+
+  // don't show pane if we're not the current pane
+  let hidden: boolean;
+  $: hidden = pane !== $navStore;
+</script>
+
+<div id={`pane-${pane}`} data-testid={testid} role="tabpanel" class:hidden>
+  <slot />
+</div>
+
+<style lang="less">
+  div {
+    grid-area: editor;
+
+    &.hidden {
+      display: none;
+    }
+  }
+</style>
diff --git a/src/lib/components/headline.svelte b/src/lib/components/headline.svelte
index a1d6c1a..409750f 100644
--- a/src/lib/components/headline.svelte
+++ b/src/lib/components/headline.svelte
@@ -1,4 +1,6 @@
-<h1><slot /></h1>
+<header>
+  <h1><slot /></h1>
+</header>
 
 <style lang="less">
   h1 {
@@ -8,4 +10,8 @@
     padding: @padding-md;
     text-align: center;
   }
+
+  header {
+    grid-area: headline;
+  }
 </style>
diff --git a/src/lib/components/nav-item.svelte b/src/lib/components/nav-item.svelte
index bc8fb46..56a8364 100644
--- a/src/lib/components/nav-item.svelte
+++ b/src/lib/components/nav-item.svelte
@@ -6,10 +6,6 @@
   let selected: boolean;
   $: selected = destination === $pane;
 
-  // we need to hide the "preview" tab on desktop
-  let hiddenOnDesktop: boolean;
-  $: hiddenOnDesktop = destination === 'preview';
-
   function handleKey({ key }: KeyboardEvent) {
     if (key === ' ' || key === 'Enter' || key === 'Spacebar') {
       navigate();
@@ -26,7 +22,6 @@
   aria-selected={selected}
   role="tab"
   tabindex="0"
-  class:hiddenOnDesktop
   class:selected
   on:click={navigate}
   on:keyup={handleKey}
@@ -36,21 +31,14 @@
 
 <style lang="less">
   li {
-    box-sizing: border-box;
-    font-weight: @weight-semibold;
+    @separator: 1px solid @color-dark;
+
+    font-weight: @font-w-semibold;
     margin: 0;
     padding: @padding-md;
     text-align: center;
     transition: background-color 0.2s;
 
-    @media screen and (min-width: @sizes[lg]) {
-      width: 50%;
-
-      &.hiddenOnDesktop {
-        display: none;
-      }
-    }
-
     &.selected {
       background-color: darken(@color-light, 20%);
     }
@@ -61,17 +49,15 @@
     }
 
     &:not(:last-of-type) {
-      @separator: 1px solid @color-dark;
-
       border-bottom: @separator;
+    }
 
-      @media screen and (min-width: @sizes[lg]) {
-        border-bottom: unset;
+    @media screen and (min-width: @sizes[lg]) {
+      flex: 1;
+
+      &:not(:last-of-type) {
+        border-bottom: none;
         border-right: @separator;
-
-        &:nth-of-type(n + 2) {
-          border-right: unset;
-        }
       }
     }
   }
diff --git a/src/lib/components/nav-toggle.svelte b/src/lib/components/nav-toggle.svelte
index 5908e1a..3365001 100644
--- a/src/lib/components/nav-toggle.svelte
+++ b/src/lib/components/nav-toggle.svelte
@@ -18,12 +18,8 @@
     border: none;
     color: @color-light;
     font-size: @font-s-md;
-    font-weight: @weight-semibold;
+    font-weight: @font-w-semibold;
     padding: @padding-sm;
     width: 100%;
-
-    @media screen and (min-width: @sizes[lg]) {
-      display: none;
-    }
   }
 </style>
diff --git a/src/lib/components/nav-tree.svelte b/src/lib/components/nav-tree.svelte
index d416cce..742bd0e 100644
--- a/src/lib/components/nav-tree.svelte
+++ b/src/lib/components/nav-tree.svelte
@@ -1,25 +1,17 @@
 <script lang="ts">
-  import { onMount } from 'svelte';
   import { slide } from 'svelte/transition';
 
-  import { navHeight } from '$lib/stores/layout';
-
   import NavToggle from './nav-toggle.svelte';
 
   export let mobile: boolean = true;
 
   let open: boolean = false;
-
-  // magic to offset preview pane on desktop
-  let navRef: HTMLElement;
-
-  onMount(() => {
-    $navHeight = navRef.getBoundingClientRect().height;
-  });
 </script>
 
-<nav bind:this={navRef}>
-  <NavToggle bind:navOpen={open} />
+<nav>
+  {#if mobile}
+    <NavToggle bind:navOpen={open} />
+  {/if}
   {#if open || !mobile}
     <ul role="tablist" transition:slide={{ duration: 300 }}>
       <slot />
@@ -28,6 +20,10 @@
 </nav>
 
 <style lang="less">
+  nav {
+    grid-area: navtree;
+  }
+
   ul {
     @border: 2px solid @color-dark;
 
@@ -38,12 +34,11 @@
     padding: 0;
 
     @media screen and (min-width: @sizes[lg]) {
-      align-items: center;
+      align-items: stretch;
       border-right: @border;
       display: flex;
       flex-direction: row;
-      justify-content: space-evenly;
-      width: 50%;
+      justify-content: space-between;
     }
   }
 </style>
diff --git a/src/lib/components/preview.svelte b/src/lib/components/preview.svelte
new file mode 100644
index 0000000..5f16222
--- /dev/null
+++ b/src/lib/components/preview.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+  import { pane } from '$lib/stores/nav';
+
+  export let mobile: boolean;
+
+  let hidden: boolean;
+  $: hidden = mobile ? $pane !== 'preview' : true;
+
+  export let markdown: string;
+  export let stylesheet: string;
+</script>
+
+<main class:hidden class:mobile data-testid="preview-pane">
+  <h2>markdown:</h2>
+  <code>{markdown}</code>
+  <h2>stylesheet:</h2>
+  <code>{stylesheet}</code>
+</main>
+
+<style lang="less">
+  main {
+    grid-area: preview;
+
+    &.mobile {
+      grid-area: editor;
+
+      &.hidden {
+        display: none;
+      }
+    }
+  }
+</style>
diff --git a/src/lib/stores/layout.ts b/src/lib/stores/layout.ts
deleted file mode 100644
index 6b7fdc0..0000000
--- a/src/lib/stores/layout.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { writable } from 'svelte/store';
-
-export const navHeight = writable(-1);
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 99fd55b..12d76a2 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,12 +1,39 @@
 <script lang="ts">
+  import { onMount } from 'svelte';
+  import { get } from 'svelte/store';
+
+  import { pane } from '$lib/stores/nav.js';
+
   import CodeInput from '$lib/components/code-input.svelte';
-  import Content from '$lib/components/content.svelte';
+  import Editor from '$lib/components/editor.svelte';
   import Headline from '$lib/components/headline.svelte';
   import NavItem from '$lib/components/nav-item.svelte';
   import NavTree from '$lib/components/nav-tree.svelte';
+  import Preview from '$lib/components/preview.svelte';
 
   export let data;
 
+  let mobile: boolean = data.mobile;
+
+  function checkIsDesktop() {
+    // TODO: figure out how to remove hard-coded value here
+    mobile = !window.matchMedia('screen and (min-width: 1280px)').matches;
+
+    if (!mobile && get(pane) === 'preview') {
+      pane.set('content');
+    }
+  }
+
+  onMount(() => {
+    checkIsDesktop();
+
+    window.addEventListener('resize', checkIsDesktop);
+
+    return () => {
+      window.removeEventListener('resize', checkIsDesktop);
+    };
+  });
+
   let markdown: string = '';
   let stylesheet: string = '';
 </script>
@@ -15,65 +42,40 @@
   <title>resumarkdown</title>
 </svelte:head>
 <div>
-  <header>
-    <Headline>resumarkdown</Headline>
-    <NavTree mobile={data.mobile}>
-      <NavItem destination="content">content</NavItem>
-      <NavItem destination="style">style</NavItem>
+  <Headline>resumarkdown</Headline>
+  <NavTree {mobile}>
+    <NavItem destination="content">content</NavItem>
+    <NavItem destination="style">style</NavItem>
+    {#if mobile}
       <NavItem destination="preview">preview</NavItem>
-    </NavTree>
-  </header>
-  <main>
-    <Content pane="content" testid="content-pane">
-      <CodeInput bind:code={markdown} />
-    </Content>
-    <Content pane="style" testid="style-pane">
-      <CodeInput bind:code={stylesheet} />
-    </Content>
-    <Content pane="preview" testid="preview-pane">
-      <dl>
-        <dt>markdown:</dt>
-        <dd>{markdown !== '' ? markdown : '???'}</dd>
-        <dt>stylesheet:</dt>
-        <dd>{stylesheet !== '' ? stylesheet : '???'}</dd>
-      </dl>
-    </Content>
-  </main>
+    {/if}
+  </NavTree>
+  <Editor pane="content" testid="content-pane">
+    <CodeInput bind:code={markdown} />
+  </Editor>
+  <Editor pane="style" testid="style-pane">
+    <CodeInput bind:code={stylesheet} />
+  </Editor>
+  <Preview {mobile} {markdown} {stylesheet} />
 </div>
 
 <style lang="less">
-  // util
-  .margin-v(@v) {
-    margin-bottom: @v;
-    margin-top: @v;
-  }
-
-  // styles
   div {
-    align-items: stretch;
-    display: flex;
-    flex-direction: column;
+    align-content: start;
+    display: grid;
+    grid-template-areas:
+      'headline'
+      'navtree'
+      'editor';
+    grid-template-rows: min-content min-content 1fr;
     height: 100%;
-    justify-content: start;
-  }
-
-  dd {
-    .margin-v(@padding-lg-y);
-  }
-
-  dl {
-    margin: 0;
-    padding: @padding-xl;
-  }
-
-  main {
-    flex-grow: 1;
 
     @media screen and (min-width: @sizes[lg]) {
-      align-items: start;
-      display: flex;
-      flex-direction: row;
-      justify-content: space-between;
+      grid-template-columns: repeat(2, 1fr);
+      grid-template-areas:
+        'headline headline'
+        'navtree preview'
+        'editor preview';
     }
   }
 </style>
diff --git a/src/styles.less b/src/styles.less
index 2c35fe9..c67e067 100644
--- a/src/styles.less
+++ b/src/styles.less
@@ -9,8 +9,8 @@
 @fonts-sans: Rubik, Arial, sans-serif;
 @font-s-lg: 2rem;
 @font-s-md: 1.25rem;
-@weight-bold: 700;
-@weight-semibold: 600;
+@font-w-bold: 700;
+@font-w-semibold: 600;
 
 // layout
 @border-r-xl: 0.75em;
diff --git a/tests/desktop.test.ts b/tests/desktop.test.ts
index dcc08ba..8f620a8 100644
--- a/tests/desktop.test.ts
+++ b/tests/desktop.test.ts
@@ -2,42 +2,21 @@ import { expect, test } from '@playwright/test';
 
 test('desktop page has nav tree', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('nav')).toBeVisible();
+  await expect(page.getByRole('navigation')).toBeVisible();
 });
 
 test('desktop page does not have nav toggle', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('nav button')).toBeHidden();
+  await expect(page.getByRole('navigation').getByRole('button')).toBeHidden();
 });
 
 test('desktop page has two-column layout', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('#pane-preview')).toBeVisible();
+  await expect(page.getByTestId('content-pane')).toBeVisible();
+  await expect(page.getByTestId('preview-pane')).toBeVisible();
 });
 
 test('desktop page has no "preview" nav item', async ({ page }) => {
   await page.goto('/');
   await expect(page.getByRole('tab').filter({ hasText: 'preview' })).toBeHidden();
 });
-
-test('desktop page has equal width for both columns', async ({ page }) => {
-  await page.goto('/');
-  const { width: contentWidth } = (await page.getByTestId('content-pane').boundingBox()) ?? {
-    width: -1,
-  };
-  const { width: previewWidth } = (await page.getByTestId('preview-pane').boundingBox()) ?? {
-    width: -2,
-  };
-  expect(contentWidth).toEqual(previewWidth);
-});
-
-test('desktop page has equal width for nav items', async ({ page }) => {
-  await page.goto('/');
-  const visibleTabs = page.getByRole('tab').filter({ hasNotText: 'preview' });
-  const { width: firstTabWidth } = (await visibleTabs.first().boundingBox()) ?? {
-    width: -1,
-  };
-  for (let i = 1; i < (await visibleTabs.count()); i++) {
-    await expect((await visibleTabs.nth(i).boundingBox())?.width).toEqual(firstTabWidth);
-  }
-});
diff --git a/tests/general.test.ts b/tests/general.test.ts
index bf617d6..298ce61 100644
--- a/tests/general.test.ts
+++ b/tests/general.test.ts
@@ -2,9 +2,7 @@ import { expect, test } from '@playwright/test';
 
 test('page has headline', async ({ page }) => {
   await page.goto('/');
-  const headline = page.locator('header h1');
-  await expect(headline).toBeVisible();
-  await expect(headline).toHaveText('resumarkdown');
+  await expect(page.getByRole('banner')).toHaveText('resumarkdown');
 });
 
 test('nav items work', async ({ page }) => {
diff --git a/tests/mobile.test.ts b/tests/mobile.test.ts
index 3189fb5..30add37 100644
--- a/tests/mobile.test.ts
+++ b/tests/mobile.test.ts
@@ -9,25 +9,26 @@ test.use({
 
 test('mobile page has nav tree hidden by default', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('nav ul')).toBeHidden();
+  await expect(page.getByRole('navigation').getByRole('tablist')).toBeHidden();
 });
 
 test('mobile page has nav toggle', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('nav button')).toBeVisible();
+  await expect(page.getByRole('navigation').getByRole('button')).toBeVisible();
 });
 
 test('nav toggle works', async ({ page }) => {
   await page.goto('/');
-  await page.locator('nav button').click();
-  await expect(page.locator('nav ul')).toBeVisible();
-  await page.locator('nav button').click();
-  await expect(page.locator('nav ul')).toBeHidden();
+  await page.getByText('show navigation').click();
+  await expect(page.getByRole('navigation').getByRole('tablist')).toBeVisible();
+  await page.getByText('hide navigation').click();
+  await expect(page.getByRole('navigation').getByRole('tablist')).toBeHidden();
 });
 
 test('mobile page has single-column layout', async ({ page }) => {
   await page.goto('/');
-  await expect(page.locator('#pane-preview')).toBeHidden();
+  await expect(page.getByTestId('content-pane')).toBeVisible();
+  await expect(page.getByTestId('preview-pane')).toBeHidden();
 });
 
 test('mobile page has preview nav item', async ({ page }) => {
