diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@
 and this project adheres to the
 [Haskell Package Versioning Policy](https://pvp.haskell.org/).
 
+## 0.1.0.2 - 2026-08-28
+
+- Keep leading spaces after a hard break and at the start of the text; only fold them after a soft wrap.
+- Leading indentation no longer counts as stretchable space for justification.
+
 ## 0.1.0.1 - 2026-08-16
 
 - Fix missing foldl' import.
diff --git a/kb-text-layout.cabal b/kb-text-layout.cabal
--- a/kb-text-layout.cabal
+++ b/kb-text-layout.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           kb-text-layout
-version:        0.1.0.1
+version:        0.1.0.2
 synopsis:       Multiline text measurement & layout.
 category:       Text
 author:         IC Rainbow
diff --git a/src/KB/Text/Layout/Break.hs b/src/KB/Text/Layout/Break.hs
--- a/src/KB/Text/Layout/Break.hs
+++ b/src/KB/Text/Layout/Break.hs
@@ -69,22 +69,19 @@
           Tab -> [plain i]
           PreservedSpace -> [plain i]
           ZeroWidthBreak -> [plain i]
-          SoftHyphen -> [Breakpoint{to = i, next = i + 1, hyphen = True, forced = False}]
+          SoftHyphen -> [Breakpoint{to = i, next = skipSpaces (i + 1), hyphen = True, forced = False}]
           HardBreak -> [Breakpoint{to = i, next = i + 1, hyphen = False, forced = True}]
-          Atomic -> [Breakpoint{to = i, next = i, hyphen = False, forced = False}, Breakpoint{to = i + 1, next = i + 1, hyphen = False, forced = False}]
+          Atomic -> [Breakpoint{to = i, next = i, hyphen = False, forced = False}, plain i]
           Word -> []
           Glue -> []
-        plain i = Breakpoint{to = i, next = i + 1, hyphen = False, forced = False}
+        plain i = Breakpoint{to = i, next = skipSpaces (i + 1), hyphen = False, forced = False}
 
     skipSpaces i
       | i < total, (segs Vector.! i).kind == Space = skipSpaces (i + 1)
       | otherwise = i
 
-    lineStart i = skipSpaces i
-
-    lineOf from cand =
+    lineOf a cand =
       let
-        a = lineStart from
         hyphenWidth = if cand.hyphen then (segs Vector.! cand.to).width else 0
         natural = prefixFlow Vector.! cand.to - prefixFlow Vector.! a + hyphenWidth
         stretchable = prefixSpace Vector.! cand.to - prefixSpace Vector.! a
@@ -206,21 +203,20 @@
 
 layoutNextLineRange :: PreparedText -> Float -> Cursor -> Maybe (LineRange, Maybe Cursor)
 layoutNextLineRange prepared maxWidth cursor
-  | start.segment >= total = Nothing
-  | otherwise = Just (scanSeg start.segment start.grapheme 0 0 Nothing)
+  | cursor.segment >= total = Nothing
+  | otherwise = Just (scanSeg cursor.segment cursor.grapheme 0 0 Nothing)
   where
     segs = prepared.segments
     total = Vector.length segs
-    start = skipSpaces cursor
 
-    skipSpaces c
+    resume c
       | c.segment < total
       , c.grapheme == 0
       , (segs Vector.! c.segment).kind == Space =
-          skipSpaces Cursor{segment = c.segment + 1, grapheme = 0}
-      | otherwise = c
+          resume Cursor{segment = c.segment + 1, grapheme = 0}
+      | otherwise = Just c
 
-    emit to width ended = LineRange{from = start, to, width, ended}
+    emit to width ended = LineRange{from = cursor, to, width, ended}
 
     scanSeg i g acc visible cand
       | i >= total = (emit (Cursor total 0) visible Finished, Nothing)
@@ -235,14 +231,14 @@
                 0
                 (acc + segWidth)
                 visible
-                (Just Candidate{to = Cursor i 0, width = visible, end = Wrapped, next = after})
+                (if visible > 0 then Just Candidate{to = Cursor i 0, width = visible, end = Wrapped, next = after} else cand)
             fitOrBreak
               | acc + segWidth <= maxWidth + epsilon =
                   scanSeg (i + 1) 0 (acc + segWidth) (acc + segWidth) cand
               | otherwise =
                   case cand of
-                    Just c -> (emit c.to c.width c.end, Just c.next)
-                    Nothing -> emergency seg i g acc
+                    Just c -> (emit c.to c.width c.end, resume c.next)
+                    Nothing -> emergency seg i g acc visible
           in
             case seg.kind of
               HardBreak -> (emit (Cursor i 0) visible HardBroken, Just after)
@@ -271,26 +267,26 @@
                      scanSeg (i + 1) 0 (acc + segWidth) (acc + segWidth) $
                        Just Candidate{to = after, width = acc + segWidth, end = Wrapped, next = after}
                    else case candBefore of
-                     Just c -> (emit c.to c.width c.end, Just c.next)
-                     Nothing -> (emit after (acc + segWidth) Overflowed, Just after)
+                     Just c -> (emit c.to c.width c.end, resume c.next)
+                     Nothing -> (emit after (acc + segWidth) Overflowed, resume after)
               Word -> fitOrBreak
               Glue -> fitOrBreak
 
-    emergency seg i g acc =
+    emergency seg i g acc visible =
       let
         ws = dropChars g seg.graphemeWidths
         counted = countFits acc ws
         count
-          | counted == 0 && acc <= epsilon = 1
+          | counted == 0 && visible <= epsilon = 1
           | otherwise = counted
         taken = take count ws
         g' = g + sum (map fst taken)
         takenWidth = sum (map snd taken)
       in
         if count == 0 then
-          (emit (Cursor i g) acc Overflowed, Just (Cursor i g))
+          (emit (Cursor i g) acc Overflowed, resume (Cursor i g))
         else
-          (emit (Cursor i g') (acc + takenWidth) Overflowed, Just (advanceCursor seg i g'))
+          (emit (Cursor i g') (acc + takenWidth) Overflowed, resume (advanceCursor seg i g'))
 
     countFits = fits 0
       where
@@ -429,8 +425,7 @@
       | otherwise = line.to.segment
     inside =
       [ seg.width
-      | j <- [line.from.segment .. min upper (Vector.length segs - 1)]
-      , let seg = segs Vector.! j
+      | seg <- dropWhile (\seg -> seg.kind == Space) (map (segs Vector.!) [line.from.segment .. min upper (Vector.length segs - 1)])
       , seg.kind == Space
       ]
 
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -117,6 +117,22 @@
             stretches w = map (Break.lineStretch prep) (Break.layoutGreedy prep w)
           stretches 200 @?= [LineStretch{spaces = 2, width = 20}]
           stretches 70 @?= [LineStretch{spaces = 1, width = 10}, LineStretch{spaces = 0, width = 0}]
+          map (Break.lineStretch (fromSegments indented)) (Break.layoutGreedy (fromSegments indented) 200)
+            @?= [LineStretch{spaces = 0, width = 0}, LineStretch{spaces = 0, width = 0}, LineStretch{spaces = 0, width = 0}]
+      , testCase "leading spaces survive hard breaks" do
+          let
+            prep = fromSegments indented
+            lns = Break.layoutGreedy prep 200
+          map (Break.materializeLineRange prep) lns @?= ["main", "do", "  putStrLn"]
+          map (\l -> (l.from, l.width)) lns @?= [(Cursor 0 0, 40), (Cursor 2 0, 20), (Cursor 4 0, 100)]
+          map (Break.materializeLineRange prep) (Break.layoutOptimal prep 200) @?= ["main", "do", "  putStrLn"]
+          materialized [space, word 40 "word"] 200 @?= [" word"]
+          materialized [indent, word 80 "putStrLn"] 90 @?= ["  putStrL", "n"]
+          stepped indented [200, 200, 200] @?= ["main", "do", "  putStrLn"]
+      , testCase "leading spaces still fold after a wrap" do
+          let prep = fromSegments [word 30 "aaa", indent, word 30 "bbb"]
+          materialized [word 30 "aaa", indent, word 30 "bbb"] 40 @?= ["aaa", "bbb"]
+          map (Break.materializeLineRange prep) (Break.layoutOptimal prep 40) @?= ["aaa", "bbb"]
       , testCase "shrinkwrap tightens without adding lines" do
           Break.shrinkwrap (fromSegments rhythm) 80 @?= 70
           Break.shrinkwrap (fromSegments rhythm) 200 @?= 110
@@ -179,8 +195,9 @@
         Nothing -> []
         Just (line, next) -> Break.materializeLineRange prep line : go next rest
 
-prepared, hyphenated, wideHyphen, broken, glued, rhythm, styled :: [MeasuredSegment]
+prepared, hyphenated, wideHyphen, broken, glued, rhythm, styled, indented :: [MeasuredSegment]
 prepared = [word 50 "hello", space, word 50 "world"]
+indented = [word 40 "main", seg HardBreak 0 "\n", word 20 "do", seg HardBreak 0 "\n", indent, word 80 "putStrLn"]
 hyphenated = [word 20 "hy", seg SoftHyphen 5 "\xAD", word 30 "phen"]
 wideHyphen = [word 20 "hy", seg SoftHyphen 15 "\xAD", word 9 "phen"]
 broken = [word 30 "one", seg HardBreak 0 "\n", word 30 "two"]
@@ -206,8 +223,9 @@
 atom :: Float -> Text -> MeasuredSegment
 atom w label = MeasuredSegment{text = label, kind = Atomic, width = w, style = 0, graphemeWidths = [(Text.length label, w)]}
 
-space :: MeasuredSegment
+space, indent :: MeasuredSegment
 space = seg Space 10 " "
+indent = seg Space 20 "  "
 
 fromSegments :: [MeasuredSegment] -> PreparedText
 fromSegments xs = PreparedText{segments = Vector.fromList xs}
